diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index d3a68d0..cc200d1 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,4 +1,4 @@ - +--- name: Feature Request about: Suggest new Feature Request for this project title: "{ISSUE_TITLE}" diff --git a/.gitignore b/.gitignore index c35d488..75aa820 100644 --- a/.gitignore +++ b/.gitignore @@ -204,5 +204,5 @@ Network Trash Folder Temporary Items .apdisk -src/main/java/controller/ +#src/main/java/controller/ diff --git a/build.gradle b/build.gradle index 4345dbb..10f395e 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,20 @@ dependencies { implementation group: 'org.apache.commons', name: 'commons-dbcp2', version: '2.9.0' // https://mvnrepository.com/artifact/com.h2database/h2 implementation group: 'com.h2database', name: 'h2', version: '2.1.210' + + // EMBED TOMCAT 관련 설정 + // tomcat-embed-core + implementation 'org.apache.tomcat.embed:tomcat-embed-core:10.1.0-M17' + // https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-logging-juli + implementation 'org.apache.tomcat.embed:tomcat-embed-logging-juli:9.0.0.M6' + // https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-jasper + implementation 'org.apache.tomcat.embed:tomcat-embed-jasper:10.1.0-M17' + // https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-jasper + implementation 'org.apache.tomcat:tomcat-jasper:10.1.0-M17' + // https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-el + implementation 'org.apache.tomcat.embed:tomcat-embed-el:10.1.0-M17' + // https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-jsp-api + implementation 'org.apache.tomcat:tomcat-jsp-api:10.1.0-M17' } test { diff --git a/src/main/java/controller/AbstractController.java b/src/main/java/controller/AbstractController.java new file mode 100644 index 0000000..e911c6e --- /dev/null +++ b/src/main/java/controller/AbstractController.java @@ -0,0 +1,25 @@ +package controller; + +import http.request.HttpRequest; +import http.request.RequestLine; +import http.response.HttpResponse; +import java.io.IOException; + +public abstract class AbstractController implements Controller{ + + @Override + public void service(HttpRequest request, HttpResponse response) throws IOException { + RequestLine requestLine = request.getRequestLine(); + if (requestLine.getHttpMethod().isGET()) { + doGet(request, response); + } + + if (requestLine.getHttpMethod().isPOST()) { + doPost(request, response); + } + } + + protected void doGet(HttpRequest request, HttpResponse response) throws IOException {} + + protected void doPost(HttpRequest request, HttpResponse response) throws IOException {} +} diff --git a/src/main/java/controller/Controller.java b/src/main/java/controller/Controller.java new file mode 100644 index 0000000..0f103bc --- /dev/null +++ b/src/main/java/controller/Controller.java @@ -0,0 +1,10 @@ +package controller; + +import http.request.HttpRequest; +import http.response.HttpResponse; +import java.io.IOException; + +public interface Controller { + + void service(HttpRequest request, HttpResponse response) throws IOException; +} diff --git a/src/main/java/controller/HomeController.java b/src/main/java/controller/HomeController.java new file mode 100644 index 0000000..15aab5c --- /dev/null +++ b/src/main/java/controller/HomeController.java @@ -0,0 +1,22 @@ +package controller; + +import db.URLDataBase; +import http.MimeType; +import http.request.HttpRequest; +import http.request.RequestLine; +import http.request.RequestURI; +import http.response.HttpResponse; +import java.io.IOException; + +public class HomeController extends AbstractController{ + + @Override + protected void doGet(HttpRequest request, HttpResponse response) throws IOException { + String url = request.getRequestURIPath(); + if (URLDataBase.contains(url) || MimeType.isSupportedExtension(url)) { + response.ok(url); + return; + } + response.ok(); + } +} diff --git a/src/main/java/controller/LoginController.java b/src/main/java/controller/LoginController.java new file mode 100644 index 0000000..dafd1b5 --- /dev/null +++ b/src/main/java/controller/LoginController.java @@ -0,0 +1,32 @@ +package controller; + +import db.UserDataBase; +import http.Cookie; +import http.request.HttpRequest; +import http.response.HttpResponse; +import java.io.IOException; +import java.util.Map; +import model.User; +import util.HttpRequestUtils; + +public class LoginController extends AbstractController{ + + private static final String TRUE = "true"; + private static final String LOGIN = "logined"; + private static final String USER_ID = "userId"; + private static final String PASSWORD = "password"; + + @Override + protected void doPost(HttpRequest request, HttpResponse response) throws IOException { + String messageBody = request.getMessageBody(); + Map parsedMessageBody = HttpRequestUtils.parseQueryString(messageBody); + String userId = parsedMessageBody.get(USER_ID); + String password = parsedMessageBody.get(PASSWORD); + User savedUser = UserDataBase.findUserById(userId); + if (UserDataBase.login(savedUser, userId, password)) { + response.found("/index.html", new Cookie(LOGIN, TRUE)); + return; + } + response.found("/user/login_failed.html"); + } +} diff --git a/src/main/java/controller/SignUpController.java b/src/main/java/controller/SignUpController.java new file mode 100644 index 0000000..4b4a450 --- /dev/null +++ b/src/main/java/controller/SignUpController.java @@ -0,0 +1,31 @@ +package controller; + +import db.UserDataBase; +import http.request.HttpRequest; +import http.response.HttpResponse; +import java.io.IOException; +import java.util.Map; +import model.User; +import util.HttpRequestUtils; + +public class SignUpController extends AbstractController{ + + private static final String USER_ID = "userId"; + private static final String PASSWORD = "password"; + private static final String NAME = "name"; + private static final String EMAIL = "email"; + + @Override + protected void doPost(HttpRequest request, HttpResponse response) throws IOException { + String messageBody = request.getMessageBody(); + Map parsedMessageBody = HttpRequestUtils.parseQueryString(messageBody); + UserDataBase.addUser( + new User( + parsedMessageBody.get(USER_ID), + parsedMessageBody.get(PASSWORD), + parsedMessageBody.get(NAME), + parsedMessageBody.get(EMAIL)) + ); + response.found("/index.html"); + } +} diff --git a/src/main/java/controller/UserController.java b/src/main/java/controller/UserController.java new file mode 100644 index 0000000..cdab5bc --- /dev/null +++ b/src/main/java/controller/UserController.java @@ -0,0 +1,36 @@ +package controller; + +import db.UserDataBase; +import http.request.HttpRequest; +import http.request.RequestHeaders; +import http.response.HttpResponse; +import java.io.IOException; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import util.HttpRequestUtils; + +public class UserController extends AbstractController{ + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + private static final String LOGIN = "logined"; + private static final String TRUE = "true"; + + @Override + protected void doGet(HttpRequest request, HttpResponse response) throws IOException { + RequestHeaders requestHeaders = request.getRequestHeaders(); + if (!requestHeaders.hasCookie()){ + log.debug("RequestHeaders has not Cookie!"); + response.ok("/user/login.html"); + return; + } + + String cookies = requestHeaders.getCookie(); + Map parsedCookies = HttpRequestUtils.parseCookies(cookies); + if (parsedCookies.get(LOGIN).equals(TRUE)) { + response.ok("/user/list.html", UserDataBase.findAll()); + return; + } + response.ok("/user/login.html"); + } +} diff --git a/src/main/java/db/UserDataBase.java b/src/main/java/db/UserDataBase.java index 39ad114..d7d4c53 100644 --- a/src/main/java/db/UserDataBase.java +++ b/src/main/java/db/UserDataBase.java @@ -1,11 +1,9 @@ package db; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; - import model.User; public class UserDataBase { diff --git a/src/main/java/handler/RequestHandler.java b/src/main/java/handler/RequestHandler.java index 4ed8d09..90b4409 100644 --- a/src/main/java/handler/RequestHandler.java +++ b/src/main/java/handler/RequestHandler.java @@ -1,106 +1,58 @@ package handler; -import db.URLDataBase; -import db.UserDataBase; -import http.request.HttpMethod; +import controller.Controller; +import controller.HomeController; +import controller.LoginController; +import controller.SignUpController; +import controller.UserController; import http.request.HttpRequest; -import http.request.RequestHeaders; -import http.request.RequestLine; -import http.request.RequestMessageBody; -import http.request.RequestURI; import http.response.HttpResponse; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; +import java.util.HashMap; import java.util.Map; -import model.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import util.HttpRequestUtils; public class RequestHandler extends Thread { + private static final Logger log = LoggerFactory.getLogger(RequestHandler.class); + private static final Map handlerMap = new HashMap<>(); + + static { + handlerMap.put("/index.html", new HomeController()); + handlerMap.put("/user/list", new UserController()); + handlerMap.put("/user/list.html", new UserController()); + handlerMap.put("/user/create", new SignUpController()); + handlerMap.put("/user/login", new LoginController()); + } private final Socket connection; + private Controller controller = new HomeController();; public RequestHandler(Socket connectionSocket) { this.connection = connectionSocket; } + @Override public void run() { log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(), connection.getPort()); try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) { HttpRequest httpRequest = HttpRequest.from(in); - HttpResponse httpResponse = HttpResponse.ok(); - RequestLine requestLine = httpRequest.getRequestLine(); - log.info("RequestLine= {}", requestLine); - RequestURI requestUri = requestLine.getRequestUri(); - String url = requestUri.getPath(); - - if (requestLine.getHttpMethod().equals(HttpMethod.GET)) { - if (URLDataBase.contains(url) || url.contains(".css") || url.contains(".js") - || url.contains(".woff") || url.contains(".ico")) { - httpResponse = HttpResponse.ok(url); - } - if (url.equals("/user/list")) { - RequestHeaders requestHeaders = httpRequest.getRequestHeaders(); - String cookies = requestHeaders.getCookie(); - log.debug("Cookie= {}", cookies); - if (cookies == null) { - httpResponse = HttpResponse.ok("/user/login.html"); - } - - if (cookies != null) { - Map parseCookies = HttpRequestUtils.parseCookies(cookies); - if (parseCookies.get("logined").equals("false")) { - httpResponse = HttpResponse.ok("/user/login.html"); - } - httpResponse = HttpResponse.ok("/user/list.html", UserDataBase.findAll()); - } - } - httpResponse.flush(out); - } - - if (requestLine.getHttpMethod().equals(HttpMethod.POST)) { - RequestMessageBody requestMessageBody = httpRequest.getRequestMessageBody(); - String messageBody = requestMessageBody.getMessageBody(); - log.debug("HTTP Message Body = {}", messageBody ); - - Map parsedMessageBody = HttpRequestUtils.parseQueryString(messageBody); - String userId = parsedMessageBody.get("userId"); - String password = parsedMessageBody.get("password"); - if (url.equals("/user/create")) { - User user = new User( - userId, password, - parsedMessageBody.get("name"), - parsedMessageBody.get("email") - ); - UserDataBase.addUser(user); - log.debug("Create User ! = {}", user); - } - - if (url.equals("/user/login")) { - User savedUser = UserDataBase.findUserById(userId); - log.debug("saved User = {}", savedUser); - if (!UserDataBase.login(savedUser, userId, password)) { - httpResponse = HttpResponse.found("/user/login_failed.html"); - httpResponse.flush(out); - log.debug("Login Fail! userId = {}", userId); - return; - } - log.debug("Login Complete! userId = {}", userId); - httpResponse = HttpResponse.found("/index.html", true); - log.info("Response Headers = {}", httpResponse.getResponseHeaders()); - httpResponse.flush(out); - } - - httpResponse = HttpResponse.found("/index.html"); + HttpResponse httpResponse = new HttpResponse(out); + String url = httpRequest.getRequestURIPath(); + + if (handlerMap.containsKey(url)) { + controller = handlerMap.get(url); + log.debug("Controller = {}", controller.getClass()); + controller.service(httpRequest, httpResponse); + return; } - httpResponse.flush(out); - + controller.service(httpRequest, httpResponse); } catch (IOException e) { log.error(e.getMessage()); } diff --git a/src/main/java/http/Cookie.java b/src/main/java/http/Cookie.java new file mode 100644 index 0000000..fe96964 --- /dev/null +++ b/src/main/java/http/Cookie.java @@ -0,0 +1,17 @@ +package http; + +public class Cookie { + + private final String key; + private final String value; + + public Cookie(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public String toString() { + return key + "=" + value + ";"; + } +} diff --git a/src/main/java/http/MimeType.java b/src/main/java/http/MimeType.java new file mode 100644 index 0000000..7fb80a2 --- /dev/null +++ b/src/main/java/http/MimeType.java @@ -0,0 +1,25 @@ +package http; + +import java.util.Arrays; + +public enum MimeType { + HTML(".html"), CSS(".css"), JS(".js"), WOFF(".woff"), ICO(".ico"), + TTF(".ttf"), EOT(".eot"), SVG(".svg"), OTF(".otf"); + + private final String extension; + + MimeType(String extension) { + this.extension = extension; + } + + public static boolean isSupportedExtension(String url) { + return searchExtension(url) != null; + } + + private static MimeType searchExtension(String url){ + return Arrays.stream(MimeType.values()) + .filter(m -> url.endsWith(m.extension)) + .findAny() + .orElse(null); + } +} diff --git a/src/main/java/http/request/HttpMethod.java b/src/main/java/http/request/HttpMethod.java index a673cdd..80435c1 100644 --- a/src/main/java/http/request/HttpMethod.java +++ b/src/main/java/http/request/HttpMethod.java @@ -1,5 +1,13 @@ package http.request; public enum HttpMethod { - GET, POST, PATCH, DELETE + GET, POST, PATCH, DELETE; + + public boolean isGET() { + return this == GET; + } + + public boolean isPOST() { + return this == POST; + } } diff --git a/src/main/java/http/request/HttpRequest.java b/src/main/java/http/request/HttpRequest.java index 4d1ceee..6fc83e8 100644 --- a/src/main/java/http/request/HttpRequest.java +++ b/src/main/java/http/request/HttpRequest.java @@ -6,9 +6,6 @@ import java.io.InputStreamReader; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import util.HttpRequestUtils; -import util.HttpRequestUtils.Pair; -import util.IOUtils; public class HttpRequest { @@ -27,24 +24,17 @@ private HttpRequest(RequestLine requestLine, RequestHeaders requestHeaders, Requ } public static HttpRequest from(InputStream in) throws IOException { - InputStreamReader inputStreamReader = new InputStreamReader(in, StandardCharsets.UTF_8); // 왜 안먹힐까? - BufferedReader bufferedReader = new BufferedReader(inputStreamReader); - String line = URLDecoder.decode(bufferedReader.readLine(), StandardCharsets.UTF_8); // 왜 한 번 더 해줘야 먹힐까? - 위에서 안먹히는 걸까? - - RequestLine requestLine = HttpRequestUtils.parseRequestLine(line); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + String line = URLDecoder.decode(bufferedReader.readLine(), StandardCharsets.UTF_8); + RequestLine requestLine = RequestLine.from(line); RequestHeaders requestHeaders = new RequestHeaders(); while (!line.equals((""))) { line = URLDecoder.decode(bufferedReader.readLine(), StandardCharsets.UTF_8); - Pair pair = HttpRequestUtils.parseHeader(line); - requestHeaders.addHeader(pair); + requestHeaders.addHeader(line); } - if (requestHeaders.containsKey("Content-Length")) { - int contentLength = Integer.parseInt(requestHeaders.getHeader("Content-Length")); - String messageBody = URLDecoder.decode( - IOUtils.readData(bufferedReader, contentLength), - StandardCharsets.UTF_8 - ); - RequestMessageBody requestMessageBody = new RequestMessageBody(messageBody); + if (requestHeaders.hasContentLength()) { + RequestMessageBody requestMessageBody = RequestMessageBody + .from(bufferedReader, requestHeaders.getContentLength()); return new HttpRequest(requestLine, requestHeaders, requestMessageBody); } return new HttpRequest(requestLine, requestHeaders); @@ -54,11 +44,15 @@ public RequestLine getRequestLine() { return requestLine; } + public String getRequestURIPath() { + return requestLine.getPath(); + } + public RequestHeaders getRequestHeaders() { return requestHeaders; } - public RequestMessageBody getRequestMessageBody() { - return requestMessageBody; + public String getMessageBody() { + return requestMessageBody.getMessageBody(); } } diff --git a/src/main/java/http/request/RequestHeaders.java b/src/main/java/http/request/RequestHeaders.java index c444ff7..b3d273c 100644 --- a/src/main/java/http/request/RequestHeaders.java +++ b/src/main/java/http/request/RequestHeaders.java @@ -2,16 +2,21 @@ import java.util.HashMap; import java.util.Map; +import util.HttpRequestUtils; import util.HttpRequestUtils.Pair; public class RequestHeaders { + private static final String CONTENT_LENGTH = "Content-Length"; + private static final String COOKIE = "Cookie"; + private final Map headers = new HashMap<>(); - public void addHeader(Pair pair) { - if (pair == null) { + public void addHeader(String line) { + if (line.isEmpty() || line.isBlank()) { return; } + Pair pair = HttpRequestUtils.parseHeader(line); headers.put(pair.getKey(), pair.getValue()); } @@ -19,14 +24,19 @@ public String getHeader(String key) { return headers.get(key); } - public boolean containsKey(String key) { - return headers.containsKey(key); + public boolean hasContentLength() { + return headers.containsKey(CONTENT_LENGTH); + } + + public int getContentLength() { + return Integer.parseInt(headers.get(CONTENT_LENGTH)); + } + + public boolean hasCookie() { + return headers.containsKey(COOKIE); } public String getCookie() { - if (headers.containsKey("Cookie")) { - return headers.get("Cookie"); - } - return null; + return headers.get(COOKIE); } } diff --git a/src/main/java/http/request/RequestLine.java b/src/main/java/http/request/RequestLine.java index abca77a..2cf0405 100644 --- a/src/main/java/http/request/RequestLine.java +++ b/src/main/java/http/request/RequestLine.java @@ -1,6 +1,7 @@ package http.request; import http.HttpVersion; +import util.HttpRequestUtils; public class RequestLine { @@ -14,7 +15,8 @@ public RequestLine(HttpMethod httpMethod, RequestURI requestUri, HttpVersion htt this.httpVersion = httpVersion; } - public static RequestLine from(String[] tokens) { + public static RequestLine from(String line) { + String[] tokens = HttpRequestUtils.parseRequestLine(line); HttpMethod httpMethod = HttpMethod.valueOf(tokens[0]); RequestURI requestURI = RequestURI.from(tokens[1]); HttpVersion httpVersion = new HttpVersion(tokens[2]); @@ -33,6 +35,10 @@ public HttpVersion getHttpVersion() { return httpVersion; } + public String getPath() { + return requestUri.getPath(); + } + @Override public String toString() { return httpMethod.toString() + " " diff --git a/src/main/java/http/request/RequestMessageBody.java b/src/main/java/http/request/RequestMessageBody.java index 750fca3..3f674a2 100644 --- a/src/main/java/http/request/RequestMessageBody.java +++ b/src/main/java/http/request/RequestMessageBody.java @@ -1,5 +1,11 @@ package http.request; +import java.io.BufferedReader; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import util.IOUtils; + public class RequestMessageBody { private final String messageBody; @@ -8,6 +14,12 @@ public RequestMessageBody(String messageBody) { this.messageBody = messageBody; } + public static RequestMessageBody from(BufferedReader bufferedReader, int contentLength) + throws IOException { + String messageBody = URLDecoder.decode(IOUtils.readData(bufferedReader, contentLength), StandardCharsets.UTF_8); + return new RequestMessageBody(messageBody); + } + public String getMessageBody() { return messageBody; } diff --git a/src/main/java/http/request/RequestURI.java b/src/main/java/http/request/RequestURI.java index d4c910d..7ced690 100644 --- a/src/main/java/http/request/RequestURI.java +++ b/src/main/java/http/request/RequestURI.java @@ -33,7 +33,7 @@ public String getQueryString() { @Override public String toString() { if (queryString != null) { - return path + queryString; + return path + "?" + queryString; } return path; } diff --git a/src/main/java/http/response/HttpResponse.java b/src/main/java/http/response/HttpResponse.java index 3e7b97e..c36bc84 100644 --- a/src/main/java/http/response/HttpResponse.java +++ b/src/main/java/http/response/HttpResponse.java @@ -1,5 +1,6 @@ package http.response; +import http.Cookie; import http.HttpVersion; import java.io.DataOutputStream; import java.io.File; @@ -12,18 +13,13 @@ public class HttpResponse { - private final StatusLine statusLine; - private final ResponseHeaders responseHeaders; + private final DataOutputStream dos; + private StatusLine statusLine; + private final ResponseHeaders responseHeaders = new ResponseHeaders(); private ResponseMessageBody responseMessageBody; - public HttpResponse(StatusLine statusLine, ResponseHeaders responseHeaders) { - this.statusLine = statusLine; - this.responseHeaders = responseHeaders; - } - - public HttpResponse(StatusLine statusLine, ResponseHeaders responseHeaders, ResponseMessageBody responseMessageBody) { - this(statusLine, responseHeaders); - this.responseMessageBody = responseMessageBody; + public HttpResponse(OutputStream out) { + this.dos = new DataOutputStream(out); } public StatusLine getStatusLine() { @@ -34,31 +30,33 @@ public ResponseHeaders getResponseHeaders() { return responseHeaders; } - public static HttpResponse ok() { - StatusLine statusLine = new StatusLine(new HttpVersion("HTTP/1.1"), HttpResponseStatus.OK); + public ResponseMessageBody getResponseMessageBody() { + return responseMessageBody; + } + + public void ok() throws IOException { + statusLine = new StatusLine(new HttpVersion("HTTP/1.1"), HttpResponseStatus.OK); byte[] messageBody = "Hello World".getBytes(StandardCharsets.UTF_8); - ResponseHeaders responseHeaders = new ResponseHeaders(); responseHeaders.setContentType("text/plain;charset=utf-8"); responseHeaders.setContentLength(messageBody.length); - ResponseMessageBody responseMessageBody = new ResponseMessageBody(messageBody); - return new HttpResponse(statusLine, responseHeaders, responseMessageBody); + responseMessageBody = new ResponseMessageBody(messageBody); + flush(); } - public static HttpResponse ok(String viewName) throws IOException { + public void ok(String viewName) throws IOException { byte[] messageBody = Files.readAllBytes(new File("./webapp" + viewName).toPath()); - StatusLine statusLine = new StatusLine(new HttpVersion("HTTP/1.1"), HttpResponseStatus.OK); + statusLine = new StatusLine(new HttpVersion("HTTP/1.1"), HttpResponseStatus.OK); String[] urlTokens = viewName.split("\\."); String extension = urlTokens[urlTokens.length - 1]; - ResponseHeaders responseHeaders = new ResponseHeaders(); responseHeaders.setContentType("text/"+extension+";charset=utf-8"); responseHeaders.setAccept("text/"+extension+", */*; q=0.1"); responseHeaders.setContentLength(messageBody.length); - ResponseMessageBody responseMessageBody = new ResponseMessageBody(messageBody); - return new HttpResponse(statusLine, responseHeaders, responseMessageBody); + responseMessageBody = new ResponseMessageBody(messageBody); + flush(); } - public static HttpResponse ok(String viewName, List users) throws IOException { + public void ok(String viewName, List users) throws IOException { StringBuilder sb = new StringBuilder(); List fileLines = Files.readAllLines(new File("./webapp" + viewName).toPath()); for (String fileLine : fileLines) { @@ -80,44 +78,30 @@ public static HttpResponse ok(String viewName, List users) throws IOExcept sb.append(fileLine).append("\r\n"); } byte[] messageBody = sb.toString().getBytes(StandardCharsets.UTF_8); - StatusLine statusLine = new StatusLine(new HttpVersion("HTTP/1.1"), HttpResponseStatus.OK); - ResponseHeaders responseHeaders = new ResponseHeaders(); + statusLine = new StatusLine(new HttpVersion("HTTP/1.1"), HttpResponseStatus.OK); responseHeaders.setContentType("text/html;charset=utf-8"); responseHeaders.setContentLength(messageBody.length); - ResponseMessageBody responseMessageBody = new ResponseMessageBody(messageBody); - return new HttpResponse(statusLine, responseHeaders, responseMessageBody); + responseMessageBody = new ResponseMessageBody(messageBody); + flush(); } - public static HttpResponse found(String redirectURI) { - StatusLine statusLine = new StatusLine(new HttpVersion("HTTP/1.1"), HttpResponseStatus.FOUND); - ResponseHeaders responseHeaders = new ResponseHeaders(); + public void found(String redirectURI) throws IOException { + statusLine = new StatusLine(new HttpVersion("HTTP/1.1"), HttpResponseStatus.FOUND); responseHeaders.setContentType("text/html;charset=utf-8"); responseHeaders.setLocation(redirectURI); - responseHeaders.setCookie(false); - return new HttpResponse(statusLine, responseHeaders); + responseHeaders.setCookie(new Cookie("logined", "false")); + flush(); } - public static HttpResponse found(String redirectURI, boolean cookie) { - StatusLine statusLine = new StatusLine(new HttpVersion("HTTP/1.1"), HttpResponseStatus.FOUND); - ResponseHeaders responseHeaders = new ResponseHeaders(); + public void found(String redirectURI, Cookie cookie) throws IOException { + statusLine = new StatusLine(new HttpVersion("HTTP/1.1"), HttpResponseStatus.FOUND); responseHeaders.setContentType("text/html;charset=utf-8"); responseHeaders.setLocation(redirectURI); responseHeaders.setCookie(cookie); - return new HttpResponse(statusLine, responseHeaders); - } - - public static HttpResponse notHTML(String extension, String viewName) throws IOException { - byte[] messageBody = Files.readAllBytes(new File("./webapp/" + viewName).toPath()); - StatusLine statusLine = new StatusLine(new HttpVersion("HTTP/1.1"), HttpResponseStatus.OK); - ResponseHeaders responseHeaders = new ResponseHeaders(); - responseHeaders.setAccept("text/"+extension+", */*; q=0.1"); - responseHeaders.setContentLength(messageBody.length); - ResponseMessageBody responseMessageBody = new ResponseMessageBody(messageBody); - return new HttpResponse(statusLine, responseHeaders, responseMessageBody); + flush(); } - public void flush(OutputStream out) throws IOException { - DataOutputStream dos = new DataOutputStream(out); + private void flush() throws IOException { dos.writeBytes(statusLine.toString() + " \r\n"); dos.writeBytes(responseHeaders.toString()); dos.writeBytes("\r\n"); diff --git a/src/main/java/http/response/ResponseHeaders.java b/src/main/java/http/response/ResponseHeaders.java index 65fd60b..ac7b6cd 100644 --- a/src/main/java/http/response/ResponseHeaders.java +++ b/src/main/java/http/response/ResponseHeaders.java @@ -1,5 +1,6 @@ package http.response; +import http.Cookie; import java.util.HashMap; import java.util.Map; @@ -27,8 +28,8 @@ public void setLocation(String redirectURI) { headers.put("Location", redirectURI); } - public void setCookie(boolean cookie) { - headers.put("Set-Cookie", "logined=" + cookie + "; path=/;"); + public void setCookie(Cookie cookie) { + headers.put("Set-Cookie", cookie + " path=/;"); } public void setAccept(String content) { diff --git a/src/main/java/http/response/ResponseMessageBody.java b/src/main/java/http/response/ResponseMessageBody.java index 7b49c64..1e7bb20 100644 --- a/src/main/java/http/response/ResponseMessageBody.java +++ b/src/main/java/http/response/ResponseMessageBody.java @@ -1,5 +1,7 @@ package http.response; +import java.nio.charset.StandardCharsets; + public class ResponseMessageBody { private final byte[] messageBody; @@ -11,4 +13,9 @@ public ResponseMessageBody(byte[] messageBody) { public byte[] getMessageBody() { return messageBody; } + + @Override + public String toString() { + return new String(messageBody, StandardCharsets.UTF_8); + } } diff --git a/src/main/java/servlet/HelloWorldServlet.java b/src/main/java/servlet/HelloWorldServlet.java new file mode 100644 index 0000000..3411a66 --- /dev/null +++ b/src/main/java/servlet/HelloWorldServlet.java @@ -0,0 +1,19 @@ +package servlet; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet("/hello") +public class HelloWorldServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + PrintWriter out = response.getWriter(); + out.print("Hello World!"); + } +} diff --git a/src/main/java/util/HttpRequestUtils.java b/src/main/java/util/HttpRequestUtils.java index dccf3b5..2ea983d 100644 --- a/src/main/java/util/HttpRequestUtils.java +++ b/src/main/java/util/HttpRequestUtils.java @@ -1,21 +1,19 @@ package util; -import http.request.RequestLine; +import com.google.common.base.Strings; +import com.google.common.collect.Maps; import java.util.Arrays; import java.util.Map; import java.util.stream.Collectors; -import com.google.common.base.Strings; -import com.google.common.collect.Maps; - public class HttpRequestUtils { /** * @param queryString은 URL에서 ? 이후에 전달되는 field1=value1&field2=value2 형식임 * @return */ - public static RequestLine parseRequestLine(String requestLine) { - return RequestLine.from(requestLine.split(" ")); + public static String[] parseRequestLine(String line) { + return line.split(" "); } public static Map parseQueryString(String queryString) { diff --git a/src/main/java/webserver/WebServer.java b/src/main/java/webserver/WebServer.java index fcac018..242baea 100644 --- a/src/main/java/webserver/WebServer.java +++ b/src/main/java/webserver/WebServer.java @@ -3,7 +3,6 @@ import handler.RequestHandler; import java.net.ServerSocket; import java.net.Socket; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/webserver/WebServerLauncher.java b/src/main/java/webserver/WebServerLauncher.java new file mode 100644 index 0000000..9c4b616 --- /dev/null +++ b/src/main/java/webserver/WebServerLauncher.java @@ -0,0 +1,31 @@ +package webserver; + +import java.io.File; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WebServerLauncher { + + private static final Logger log = LoggerFactory.getLogger(WebServerLauncher.class); + + public static void main(String[] args) throws LifecycleException { + String webappDirLocation = "webapp/"; + Tomcat tomcat = new Tomcat(); + tomcat.setPort(8080); +// String webPort = System.getenv("PORT"); +// if (webPort == null || webPort.isEmpty()) { +// webPort = "8080"; +// } + + Connector connector = tomcat.getConnector(); + connector.setURIEncoding("UTF-8"); + tomcat.addWebapp("/", new File(webappDirLocation).getAbsolutePath()); + log.info("configuring app with basedir: {}", new File("./" + webappDirLocation).getAbsolutePath()); + tomcat.start(); + tomcat.getServer().await(); + } + +} diff --git a/src/test/java/http/request/HttpRequestTest.java b/src/test/java/http/request/HttpRequestTest.java new file mode 100644 index 0000000..d847486 --- /dev/null +++ b/src/test/java/http/request/HttpRequestTest.java @@ -0,0 +1,87 @@ +package http.request; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("HttpRequest 클래스") +class HttpRequestTest { + + @Nested + @DisplayName("from 메소드는") + class Describe_from { + + @Nested + @DisplayName("[HTTP GET REQUEST] InputStream 객체를 인자로 받으면") + class Context_with_GET_InputStream_instance { + + @Test + @DisplayName("새로운 GET HttpRequest 객체를 반환한다.") + void it_returns_new_GET_HttpRequest_instance() throws IOException { + //given + String str = "GET /index.html HTTP/1.1\r\n" + + "Host: localhost:8080\r\n" + + "Connection: keep-alive\r\n" + + "Accept: */*\r\n" + + "\r\n"; + + //when + ByteArrayInputStream in = new ByteArrayInputStream(str.getBytes()); + HttpRequest httpRequest = HttpRequest.from(in); + RequestLine requestLine = httpRequest.getRequestLine(); + RequestHeaders requestHeaders = httpRequest.getRequestHeaders(); + + //then + assertThat(requestLine.getHttpMethod()).isEqualTo(HttpMethod.GET); + assertThat(requestLine.getRequestUri()).hasToString("/index.html"); + assertThat(requestHeaders.getHeader("Host")).isEqualTo("localhost:8080"); + assertThat(requestHeaders.getHeader("Connection")).isEqualTo("keep-alive"); + assertThat(requestHeaders.getHeader("Accept")).isEqualTo("*/*"); + assertThat(requestHeaders.hasContentLength()).isFalse(); + } + } + + @Nested + @DisplayName("[HTTP POST REQUEST] InputStream 객체를 인자로 받으면") + class Context_with_POST_InputStream_instance { + + @Test + @DisplayName("새로운 POST HttpRequest 객체를 반환한다.") + void it_returns_new_POST_HttpRequest_instance() throws IOException { + //given + String str = "POST /user/create HTTP/1.1\r\n" + + "Host: localhost:8080\r\n" + + "Connection: keep-alive\r\n" + + "Content-Length: 58\r\n" + + "Content-Type: application/x-www-form-urlencoded\r\n" + + "Accept: */*\r\n" + + "\r\n" + +"userId=nathan&password=123123&name=나단&email=nathan@dev.com"; + + //when + ByteArrayInputStream in = new ByteArrayInputStream(str.getBytes()); + HttpRequest httpRequest = HttpRequest.from(in); + RequestLine requestLine = httpRequest.getRequestLine(); + RequestHeaders requestHeaders = httpRequest.getRequestHeaders(); + String requestMessageBody = httpRequest.getMessageBody(); + + //then + assertThat(requestLine.getHttpMethod()).isEqualTo(HttpMethod.POST); + assertThat(requestLine.getRequestUri()).hasToString("/user/create"); + assertThat(requestHeaders.getHeader("Host")).isEqualTo("localhost:8080"); + assertThat(requestHeaders.getHeader("Connection")).isEqualTo("keep-alive"); + assertThat(requestHeaders.getHeader("Accept")).isEqualTo("*/*"); + assertThat(requestHeaders.hasContentLength()).isTrue(); + assertThat(requestHeaders.getContentLength()).isEqualTo(58); + assertThat(requestHeaders.getHeader("Content-Type")).isEqualTo( + "application/x-www-form-urlencoded"); + assertThat(httpRequest.getMessageBody()).isEqualTo( + "userId=nathan&password=123123&name=나단&email=nathan@dev.com"); + } + } + } +} diff --git a/src/test/java/http/request/RequestHeadersTest.java b/src/test/java/http/request/RequestHeadersTest.java new file mode 100644 index 0000000..6a06a2c --- /dev/null +++ b/src/test/java/http/request/RequestHeadersTest.java @@ -0,0 +1,35 @@ +package http.request; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("RequestHeaders 클래스") +class RequestHeadersTest { + + @Nested + @DisplayName("addHeader 메소드는") + class Describe_addHeader { + + @Nested + @DisplayName("String(line)을 인자로 받으면") + class Context_with_String { + + @Test + @DisplayName("headers에 값을 key, value로 넣는다") + void it_puts_in_headers() { + + RequestHeaders requestHeaders = new RequestHeaders(); + requestHeaders.addHeader("Host: localhost:8080"); + requestHeaders.addHeader("Connection: keep-alive"); + requestHeaders.addHeader("Accept: */*"); + + assertThat(requestHeaders.getHeader("Host")).isEqualTo("localhost:8080"); + assertThat(requestHeaders.getHeader("Connection")).isEqualTo("keep-alive"); + assertThat(requestHeaders.getHeader("Accept")).isEqualTo("*/*"); + } + } + } +} diff --git a/src/test/java/http/request/RequestLineTest.java b/src/test/java/http/request/RequestLineTest.java new file mode 100644 index 0000000..9e13ea7 --- /dev/null +++ b/src/test/java/http/request/RequestLineTest.java @@ -0,0 +1,36 @@ +package http.request; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("RequestLine 클래스") +class RequestLineTest { + + @Nested + @DisplayName("from 메소드는") + class Describe_from{ + + @Nested + @DisplayName("파싱된 Request Line이 주어진다면") + class Context_with_parsed_RequestLine { + + @Test + @DisplayName("RequestLine 객체를 반환한다") + void it_returns_a_RequestLine() { + String rawHttpRequestLine = "GET /index.html?userId=3333 HTTP/1.1"; + RequestLine requestLine = RequestLine.from(rawHttpRequestLine); + + assertThat(requestLine.getHttpMethod()).isEqualTo(HttpMethod.GET); + assertThat(requestLine.getRequestUri()).hasToString("/index.html?userId=3333"); + assertThat(requestLine.getHttpVersion().getVersion()).isEqualTo("HTTP/1.1"); + + } + + } + } + + +} diff --git a/src/test/java/http/request/RequestMessageBodyTest.java b/src/test/java/http/request/RequestMessageBodyTest.java new file mode 100644 index 0000000..5f27700 --- /dev/null +++ b/src/test/java/http/request/RequestMessageBodyTest.java @@ -0,0 +1,42 @@ +package http.request; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("RequestMessageBody 클래스") +class RequestMessageBodyTest { + + @Nested + @DisplayName("from 메소드는") + class Describe_from { + + @Nested + @DisplayName("BufferedReader 객체와 String(ContentLength)를 인자로 받으면") + class Context_with_BufferedRader_and_String { + + @Test + @DisplayName("새로운 RequestMessageBody 객체를 반환한다") + void it_returns_new_RequestMessageBody_instance() throws IOException { + //given + String rawMessageBody = "userId=nathan&password=123123&name=나단&email=phs5731@nav.com"; + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(rawMessageBody.getBytes()); + BufferedReader br = new BufferedReader(new InputStreamReader(byteArrayInputStream)); + + //when + RequestMessageBody requestMessageBody = RequestMessageBody.from(br, rawMessageBody.length()); + + //then + Assertions.assertThat(requestMessageBody.getMessageBody()) + .isEqualTo("userId=nathan&password=123123&name=나단&email=phs5731@nav.com"); + } + } + } + +} diff --git a/src/test/java/http/response/HttpResponseTest.java b/src/test/java/http/response/HttpResponseTest.java new file mode 100644 index 0000000..12df118 --- /dev/null +++ b/src/test/java/http/response/HttpResponseTest.java @@ -0,0 +1,144 @@ +package http.response; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import http.Cookie; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("HttpResponse 클래스") +class HttpResponseTest { + + @Nested + @DisplayName("[HTTP RESPONSE 200 OK] ok 메소드는") + class Describe_ok { + + @Nested + @DisplayName("아무것도 인자를 받지 않으면") + class Context_with_null { + + @Test + @DisplayName("default 200 OK HttpResponse 객체를 반환한다.") + void dit_returns_default_200_OK_HttpResponse_instance() throws IOException { + //given + ByteArrayOutputStream out = new ByteArrayOutputStream(); + HttpResponse response = new HttpResponse(out); + + //when + response.ok(); + StatusLine statusLine = response.getStatusLine(); + ResponseHeaders responseHeaders = response.getResponseHeaders(); + + //then + assertThat(statusLine).hasToString("HTTP/1.1 200 OK"); + assertThat(responseHeaders.getHeader("Content-Type")).isEqualTo( + "text/plain;charset=utf-8"); + assertThat(responseHeaders.getHeader("Content-Length")).isEqualTo("11"); + } + + } + + @Nested + @DisplayName("String(viewName)을 하나만 받을 때") + class Context_with_a_String { + + @Test + @DisplayName("해당 viewName의 파일이 있다면, 그 파일을 담은 200 OK HttpResponse 객체를 반환한다.") + void it_returns_viewName_OK_HttpResponse_instance() throws IOException { + //given + String viewName = "/index.html"; + byte[] messageBody = Files.readAllBytes(new File("./webapp" + viewName).toPath()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + HttpResponse response = new HttpResponse(out); + + //when + response.ok(viewName); + StatusLine statusLine = response.getStatusLine(); + ResponseHeaders responseHeaders = response.getResponseHeaders(); + + //then + assertThat(statusLine).hasToString("HTTP/1.1 200 OK"); + assertThat(responseHeaders.getHeader("Content-Type")).isEqualTo( + "text/html;charset=utf-8"); + assertThat(responseHeaders.getHeader("Accept")).isEqualTo("text/html, */*; q=0.1"); + assertThat(responseHeaders.getHeader("Content-Length")).isEqualTo(String.valueOf(messageBody.length)); + } + + @Test + @DisplayName("해당 viewName의 파일이 없다면, NoSuchFileException을 던진다.") + void it_throws_NoSuchFileException() throws IOException { + //given + String viewName = "/indexing.html"; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + HttpResponse response = new HttpResponse(out); + + //then + assertThatThrownBy(() -> response.ok(viewName)).isInstanceOf( + NoSuchFileException.class); + } + + } + } + + @Nested + @DisplayName("[HTTP RESPONSE 302 Found] found 메소드는") + class Discribe_found{ + + @Nested + @DisplayName("String(redirectURI)를 하나만 받으면") + class Context_with_a_String { + + @Test + @DisplayName("해당 인자를 Location으로 갖는 302 Found HttpResponse 객체를 반환한다.") + void it_returns_302_Found_HttpResponse_instance() throws IOException { + //given + String redirectURI = "/index.html"; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + HttpResponse response = new HttpResponse(out); + + //when + response.found(redirectURI); + StatusLine statusLine = response.getStatusLine(); + ResponseHeaders responseHeaders = response.getResponseHeaders(); + + + //then + assertThat(statusLine).hasToString("HTTP/1.1 302 Found"); + assertThat(responseHeaders.getHeader("Location")).isEqualTo("/index.html"); + } + } + + @Nested + @DisplayName("String(redirectURI)와 boolean(Cookie)를 받으면") + class Context_with_a_String_and_a_Cookie { + + @Test + @DisplayName("해당 인자를 Location으로 갖고, Cookie가 세팅된 302 Found HttpResponse 객체를 반환한다.") + void it_returns_302_Found_HttpResponse_instance_with_Cookie() throws IOException { + //given + String redirectURI = "/index.html"; + Cookie cookie = new Cookie("logined", "true"); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + HttpResponse response = new HttpResponse(out); + + //when + response.found(redirectURI, cookie); + StatusLine statusLine = response.getStatusLine(); + ResponseHeaders responseHeaders = response.getResponseHeaders(); + + + //then + assertThat(statusLine).hasToString("HTTP/1.1 302 Found"); + assertThat(responseHeaders.getHeader("Location")).isEqualTo("/index.html"); + assertThat(responseHeaders.getHeader("Set-Cookie")).isEqualTo("logined=true; path=/;"); + } + } + } +} diff --git a/src/test/java/util/HttpRequestUtilsTest.java b/src/test/java/util/HttpRequestUtilsTest.java index 54cc874..dd32cbe 100644 --- a/src/test/java/util/HttpRequestUtilsTest.java +++ b/src/test/java/util/HttpRequestUtilsTest.java @@ -17,7 +17,7 @@ public class HttpRequestUtilsTest { @Test void RequestLine을_파싱하여_분류할_수_있다() { String line ="GET /index.html HTTP/1.1"; - RequestLine requestLine = HttpRequestUtils.parseRequestLine(line); + RequestLine requestLine = RequestLine.from(line); RequestURI requestUri = requestLine.getRequestUri(); HttpVersion httpVersion = requestLine.getHttpVersion(); diff --git a/webapp/index.html b/webapp/index.html index 226a521..c3baac6 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -44,7 +44,7 @@
  • Facebook
  • -
  • +
  • diff --git a/webapp/user/form.html b/webapp/user/form.html index 587a9d6..170f565 100644 --- a/webapp/user/form.html +++ b/webapp/user/form.html @@ -39,7 +39,7 @@
  • Facebook
  • -
  • +
  • diff --git a/webapp/user/list.html b/webapp/user/list.html index e98f978..e22d04b 100644 --- a/webapp/user/list.html +++ b/webapp/user/list.html @@ -39,7 +39,7 @@
  • Facebook
  • -
  • +
  • diff --git a/webapp/user/login.html b/webapp/user/login.html index 69be6c8..20c691e 100644 --- a/webapp/user/login.html +++ b/webapp/user/login.html @@ -39,7 +39,7 @@
  • Facebook
  • -
  • +
  • @@ -97,4 +97,4 @@ - \ No newline at end of file + diff --git a/webapp/user/login_failed.html b/webapp/user/login_failed.html index d115528..bae95ad 100644 --- a/webapp/user/login_failed.html +++ b/webapp/user/login_failed.html @@ -39,7 +39,7 @@
  • Facebook
  • -
  • +
  • @@ -98,4 +98,4 @@ - \ No newline at end of file + diff --git a/webapp/user/profile.html b/webapp/user/profile.html index dce83b1..9daf2f7 100644 --- a/webapp/user/profile.html +++ b/webapp/user/profile.html @@ -39,7 +39,7 @@
  • Facebook
  • -
  • +
  • @@ -101,4 +101,4 @@

    자바지기

    - \ No newline at end of file +