diff --git a/README.md b/README.md index bc9c93b..5d52e80 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # 자바 웹 서버 +https://github.com/ChoiGiSung/java-was/wiki + ## 진행 방법 * 요구사항에 대한 구현을 완료한 후 자신의 **github 아이디**에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다. diff --git a/src/main/java/annotation/RequestMapping.java b/src/main/java/annotation/RequestMapping.java new file mode 100644 index 0000000..975b6bd --- /dev/null +++ b/src/main/java/annotation/RequestMapping.java @@ -0,0 +1,16 @@ +package annotation; + +import http.HttpMethod; +import http.HttpRequest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface RequestMapping { + String path(); + HttpMethod method(); +} diff --git a/src/main/java/controller/Controller.java b/src/main/java/controller/Controller.java new file mode 100644 index 0000000..4a517ef --- /dev/null +++ b/src/main/java/controller/Controller.java @@ -0,0 +1,10 @@ +package controller; + +import http.HttpRequest; +import http.HttpResponse; + +import java.io.IOException; + +public interface Controller { + void service(HttpRequest httpRequest, HttpResponse httpResponse) throws IOException; +} diff --git a/src/main/java/controller/ControllerKey.java b/src/main/java/controller/ControllerKey.java new file mode 100644 index 0000000..b2fe11d --- /dev/null +++ b/src/main/java/controller/ControllerKey.java @@ -0,0 +1,29 @@ +package controller; + +import http.HttpMethod; + +import java.util.Objects; + +public class ControllerKey { + + private HttpMethod method; + private String url; + + public ControllerKey(HttpMethod method, String url) { + this.method = method; + this.url = url; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ControllerKey that = (ControllerKey) o; + return method == that.method && Objects.equals(url, that.url); + } + + @Override + public int hashCode() { + return Objects.hash(method, url); + } +} diff --git a/src/main/java/controller/CreateUserController.java b/src/main/java/controller/CreateUserController.java new file mode 100644 index 0000000..a9175b4 --- /dev/null +++ b/src/main/java/controller/CreateUserController.java @@ -0,0 +1,25 @@ +package controller; + +import annotation.RequestMapping; +import db.DataBase; +import http.HttpMethod; +import http.HttpRequest; +import http.HttpResponse; +import model.User; + +import java.io.IOException; + +@RequestMapping(path = "/user/create", method = HttpMethod.POST) +public class CreateUserController implements Controller { + @Override + public void service(HttpRequest httpRequest, HttpResponse httpResponse) throws IOException { + User user = new User( + httpRequest.data("userId"), + httpRequest.data("password"), + httpRequest.data("name"), + httpRequest.data("email") + ); + DataBase.addUser(user); + httpResponse.redirect("/index.html"); + } +} diff --git a/src/main/java/controller/DefaultController.java b/src/main/java/controller/DefaultController.java new file mode 100644 index 0000000..7204764 --- /dev/null +++ b/src/main/java/controller/DefaultController.java @@ -0,0 +1,14 @@ +package controller; + +import http.HttpRequest; +import http.HttpResponse; + +import java.io.IOException; + +public class DefaultController implements Controller { + + @Override + public void service(HttpRequest httpRequest, HttpResponse httpResponse) throws IOException { + httpResponse.forward(httpRequest.getUrl()); + } +} diff --git a/src/main/java/controller/ListUserController.java b/src/main/java/controller/ListUserController.java new file mode 100644 index 0000000..8726e8d --- /dev/null +++ b/src/main/java/controller/ListUserController.java @@ -0,0 +1,20 @@ +package controller; + +import annotation.RequestMapping; +import http.HttpMethod; +import http.HttpRequest; +import http.HttpResponse; + +import java.io.IOException; + +@RequestMapping(path = "/user/list", method = HttpMethod.GET) +public class ListUserController implements Controller { + @Override + public void service(HttpRequest httpRequest, HttpResponse httpResponse) throws IOException { + if ("true".equals(httpRequest.cookie("logined"))) { + httpResponse.forward("/user/list.html"); + return; + } + httpResponse.redirect("/user/login.html"); + } +} diff --git a/src/main/java/controller/LoginController.java b/src/main/java/controller/LoginController.java new file mode 100644 index 0000000..bf1fd3d --- /dev/null +++ b/src/main/java/controller/LoginController.java @@ -0,0 +1,27 @@ +package controller; + +import annotation.RequestMapping; +import db.DataBase; +import http.HttpMethod; +import http.HttpRequest; +import http.HttpResponse; +import model.User; + +import java.io.IOException; + +@RequestMapping(path = "/user/login", method = HttpMethod.POST) +public class LoginController implements Controller { + @Override + public void service(HttpRequest httpRequest, HttpResponse httpResponse) throws IOException { + User user = DataBase.findUserById(httpRequest.data("userId")); + + if (user != null && user.checkPassword(httpRequest.data("password"))) { + httpResponse.addHeader("Set-Cookie", "logined=true; Path=/"); + httpResponse.redirect("/index.html"); + return; + } + + httpResponse.addHeader("Set-Cookie", "logined=false; Path=/"); + httpResponse.redirect("/user/login_failed.html"); + } +} diff --git a/src/main/java/webserver/HttpMethod.java b/src/main/java/http/HttpMethod.java similarity index 70% rename from src/main/java/webserver/HttpMethod.java rename to src/main/java/http/HttpMethod.java index cf5eff5..2e1c610 100644 --- a/src/main/java/webserver/HttpMethod.java +++ b/src/main/java/http/HttpMethod.java @@ -1,4 +1,4 @@ -package webserver; +package http; public enum HttpMethod { GET, diff --git a/src/main/java/webserver/HttpRequest.java b/src/main/java/http/HttpRequest.java similarity index 72% rename from src/main/java/webserver/HttpRequest.java rename to src/main/java/http/HttpRequest.java index f4278fc..307f6a2 100644 --- a/src/main/java/webserver/HttpRequest.java +++ b/src/main/java/http/HttpRequest.java @@ -1,4 +1,4 @@ -package webserver; +package http; import util.HttpRequestUtils; import util.IOUtils; @@ -22,15 +22,32 @@ public class HttpRequest { private String body; private HttpRequest() { - } - public String getUrl() { return url; } - public void addStartLine(String buffer) { + public static HttpRequest of(InputStream in) throws IOException { + HttpRequest httpRequest = new HttpRequest(); + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + String buffer; + + buffer = br.readLine(); + httpRequest.addStartLine(buffer); + while (!(buffer = br.readLine()).equals("")) { + httpRequest.addHeaders(buffer); + } + + String contentLength = httpRequest.header("Content-Length"); + if (contentLength != null) { + httpRequest.addBody(IOUtils.readData(br, Integer.parseInt(contentLength))); + } + + return httpRequest; + } + + private void addStartLine(String buffer) { String[] startLine = buffer.split(" "); method = HttpMethod.valueOf(startLine[0].toUpperCase()); url = startLine[1]; @@ -44,7 +61,7 @@ public void addStartLine(String buffer) { } } - public void addHeaders(String buffer) { + private void addHeaders(String buffer) { HttpRequestUtils.Pair pair = HttpRequestUtils.parseHeader(buffer); headers.put(pair.getKey(), pair.getValue()); String cookies; @@ -53,31 +70,6 @@ public void addHeaders(String buffer) { } } - public static HttpRequest of(InputStream in) { - HttpRequest httpRequest = new HttpRequest(); - BufferedReader br = new BufferedReader(new InputStreamReader(in)); - String buffer; - try { - buffer = br.readLine(); - httpRequest.addStartLine(buffer); - while (!(buffer = br.readLine()).equals("")) { - httpRequest.addHeaders(buffer); - } - - String contentLength = httpRequest.header("Content-Length"); - if (contentLength != null) { - httpRequest.addBody(IOUtils.readData(br, Integer.parseInt(contentLength))); - } - - - } catch (IOException e) { - e.printStackTrace(); - } - - - return httpRequest; - } - private void addBody(String readData) { body = readData; if (method == HttpMethod.POST) { @@ -96,7 +88,7 @@ public String cookie(String key) { return cookies.get(key); } - public HttpMethod getMethod() { + public HttpMethod method() { return method; } @@ -104,7 +96,4 @@ public String data(String key) { return data.get(key); } -// public boolean loginCookie(){ -// -// } } diff --git a/src/main/java/http/HttpResponse.java b/src/main/java/http/HttpResponse.java new file mode 100644 index 0000000..8f945e6 --- /dev/null +++ b/src/main/java/http/HttpResponse.java @@ -0,0 +1,61 @@ +package http; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; + +public class HttpResponse { + + private DataOutputStream dos; + + private String startLine; + private Map headers = new HashMap<>(); + + public HttpResponse(OutputStream os) { + dos = new DataOutputStream(os); + } + + public void forward(String url) throws IOException { + startLine = "HTTP/1.1 200 OK \r\n"; + byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath()); + + String contentTypeValue = "text/html;charset=utf-8"; + if (url.endsWith(".css")) { + contentTypeValue = "text/css;charset=utf-8"; + } + + addHeader("Content-Type", contentTypeValue); + addHeader("Content-Length", String.valueOf(body.length)); + + processHeaders(); + responseBody(body); + } + + private void responseBody(byte[] body) throws IOException { + dos.write(body, 0, body.length); + dos.flush(); + } + + public void redirect(String url) throws IOException { + startLine = "HTTP/1.1 302 FOUND \r\n"; + addHeader("Location", url); + processHeaders(); + } + + public void addHeader(String header, String value) { + headers.put(header, value); + } + + private void processHeaders() throws IOException { + dos.writeBytes(startLine); + for (Map.Entry header : headers.entrySet()) { + dos.writeBytes(header.getKey() + ": " + header.getValue() + "\r\n"); + } + dos.writeBytes("\r\n"); + } + +} diff --git a/src/main/java/webserver/HandlerAdapter.java b/src/main/java/webserver/HandlerAdapter.java new file mode 100644 index 0000000..df86aab --- /dev/null +++ b/src/main/java/webserver/HandlerAdapter.java @@ -0,0 +1,34 @@ +package webserver; + +import annotation.RequestMapping; +import controller.Controller; +import controller.ControllerKey; +import controller.DefaultController; +import org.reflections.Reflections; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class HandlerAdapter { + private static final Controller defaultController = new DefaultController(); + private static final Map controllerMap = new HashMap<>(); + + public HandlerAdapter() { + Reflections reflections = new Reflections("controller"); + Set> requestMappingControllers = reflections.getTypesAnnotatedWith(RequestMapping.class); + for (Class controller : requestMappingControllers) { + RequestMapping annotation = controller.getAnnotation(RequestMapping.class); + try { + controllerMap.put(new ControllerKey(annotation.method(), annotation.path()), (Controller) controller.newInstance()); + } catch (InstantiationException | IllegalAccessException e) { + e.printStackTrace(); + } + + } + } + + public Controller controller(ControllerKey key){ + return controllerMap.getOrDefault(key,defaultController); + } +} diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index bc57a83..3628e7e 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -2,11 +2,12 @@ import java.io.*; import java.net.Socket; -import java.nio.file.Files; -import java.util.List; +import java.util.Map; -import db.DataBase; -import model.User; +import controller.Controller; +import controller.ControllerKey; +import http.HttpRequest; +import http.HttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,9 +15,12 @@ public class RequestHandler extends Thread { private static final Logger log = LoggerFactory.getLogger(RequestHandler.class); private Socket connection; + private HandlerAdapter adapter; - public RequestHandler(Socket connectionSocket) { + + public RequestHandler(Socket connectionSocket,HandlerAdapter adapter) { this.connection = connectionSocket; + this.adapter = adapter; } public void run() { @@ -24,99 +28,16 @@ public void run() { connection.getPort()); try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) { - // TODO 사용자 요청에 대한 처리는 이 곳에 구현하면 된다. HttpRequest httpRequest = HttpRequest.of(in); - String url = httpRequest.getUrl(); - - - DataOutputStream dos = new DataOutputStream(out); - if ("/user/create".equals(url)) { - User user = new User( - httpRequest.data("userId"), - httpRequest.data("password"), - httpRequest.data("name"), - httpRequest.data("email") - ); - DataBase.addUser(user); - log.debug("user : {}", user); - response302Header(dos, "/index.html"); - } else if ("/user/login".equals(url)) { - User user = DataBase.findUserById(httpRequest.data("userId")); - if (user == null) { - log.debug("Not Found"); - response302HeaderWithCookie(dos, "/user/login_failed.html", "logined=false", "/"); - } else if (user.checkPassword(httpRequest.data("password"))) { - log.debug("Login success"); - response302HeaderWithCookie(dos, "/index.html", "logined=true", "/"); - } else { - log.debug("Password was not matched"); - response302HeaderWithCookie(dos, "/user/login_failed.html", "logined=false", "/"); - } - } else if ("/user/list".equals(url)) { - log.debug("Cookie : {}", httpRequest.header("Cookie")); - if ("true".equals(httpRequest.cookie("logined"))) { - byte[] body = Files.readAllBytes(new File("./webapp" + "/user/list.html").toPath()); - response200Header(dos, body.length, "html"); - responseBody(dos, body); - } else { - response302Header(dos, "/user/login.html"); - } - } else { - log.debug("Cookie : {}", httpRequest.header("Cookie")); - byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath()); + HttpResponse httpResponse = new HttpResponse(out); - if (url.endsWith(".css")){ - response200Header(dos, body.length, "css"); - }else { - response200Header(dos, body.length, "html"); - } - - responseBody(dos, body); - } - - } catch (IOException e) { - log.error(e.getMessage()); - } - } - - private void response302HeaderWithCookie(DataOutputStream dos, String url, String cookie, String cookiePath) { - try { - dos.writeBytes("HTTP/1.1 302 FOUND \r\n"); - dos.writeBytes("Location: " + url + "\r\n"); - dos.writeBytes("Set-Cookie: " + cookie + "; Path=" + cookiePath + "\r\n"); - dos.writeBytes("\r\n"); - } catch (IOException e) { - log.error(e.getMessage()); - } - } + Controller controller = adapter.controller(new ControllerKey(httpRequest.method(), httpRequest.getUrl())); - private void response302Header(DataOutputStream dos, String url) { - try { - dos.writeBytes("HTTP/1.1 302 FOUND \r\n"); - dos.writeBytes("Location: " + url + "\r\n"); - dos.writeBytes("\r\n"); - } catch (IOException e) { - log.error(e.getMessage()); - } - } + controller.service(httpRequest, httpResponse); - private void response200Header(DataOutputStream dos, int lengthOfBodyContent, String contentType) { - try { - dos.writeBytes("HTTP/1.1 200 OK \r\n"); - dos.writeBytes("Content-Type: text/" + contentType + ";charset=utf-8\r\n"); - dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n"); - dos.writeBytes("\r\n"); } catch (IOException e) { log.error(e.getMessage()); } } - private void responseBody(DataOutputStream dos, byte[] body) { - try { - dos.write(body, 0, body.length); - dos.flush(); - } catch (IOException e) { - log.error(e.getMessage()); - } - } } diff --git a/src/main/java/webserver/WebServer.java b/src/main/java/webserver/WebServer.java index 986e1be..b072bd4 100644 --- a/src/main/java/webserver/WebServer.java +++ b/src/main/java/webserver/WebServer.java @@ -1,35 +1,39 @@ package webserver; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.ArrayList; -import java.util.List; - -import model.User; +import annotation.RequestMapping; +import controller.*; +import http.HttpMethod; +import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + public class WebServer { private static final Logger log = LoggerFactory.getLogger(WebServer.class); private static final int DEFAULT_PORT = 8080; + public static void main(String args[]) throws Exception { - int port = 0; - if (args == null || args.length == 0) { - port = DEFAULT_PORT; - } else { + int port = DEFAULT_PORT; + if (args != null && args.length > 0) { port = Integer.parseInt(args[0]); } + HandlerAdapter adapter = new HandlerAdapter(); - // 서버소켓을 생성한다. 웹서버는 기본적으로 8080번 포트를 사용한다. + // 서버소켓을 생성한다. 웹서버는 기본적으로 8080번 포트를 사용한다. try (ServerSocket listenSocket = new ServerSocket(port)) { log.info("Web Application Server started {} port.", port); // 클라이언트가 연결될때까지 대기한다. Socket connection; while ((connection = listenSocket.accept()) != null) { - RequestHandler requestHandler = new RequestHandler(connection); + RequestHandler requestHandler = new RequestHandler(connection,adapter); requestHandler.start(); } } diff --git a/src/test/java/annotation/ReflectionTest.java b/src/test/java/annotation/ReflectionTest.java new file mode 100644 index 0000000..14e2070 --- /dev/null +++ b/src/test/java/annotation/ReflectionTest.java @@ -0,0 +1,24 @@ +package annotation; + +import org.junit.jupiter.api.Test; +import org.reflections.Reflections; + +import java.lang.annotation.Annotation; +import java.util.Set; + +public class ReflectionTest { + + @Test + void getAnnotation() throws ClassNotFoundException { + Class aClass = Class.forName("annotation.RequestMapping"); + + + Reflections reflections = new Reflections("controller"); + Set> getAno = reflections.getTypesAnnotatedWith(RequestMapping.class); + + for (Class aClass1 : getAno) { + RequestMapping declaredAnnotation = aClass1.getDeclaredAnnotation(RequestMapping.class); + System.out.println(declaredAnnotation); + } + } +} diff --git a/src/test/java/controller/ControllerKeyTest.java b/src/test/java/controller/ControllerKeyTest.java new file mode 100644 index 0000000..f21de4d --- /dev/null +++ b/src/test/java/controller/ControllerKeyTest.java @@ -0,0 +1,45 @@ +package controller; + +import http.HttpMethod; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ControllerKeyTest { + + private SoftAssertions softly; + + @BeforeEach + public void beforeEach() { + softly = new SoftAssertions(); + } + + @AfterEach + public void afterEach() { + softly.assertAll(); + } + + @Test + @DisplayName("get /index.html") + void getIndex() { + String url = "/index.html"; + ControllerKey key1 = new ControllerKey(HttpMethod.GET, url); + ControllerKey key2 = new ControllerKey(HttpMethod.GET, url); + + softly.assertThat(key1).isEqualTo(key2); + softly.assertThat(key1.hashCode()).isEqualTo(key2.hashCode()); + } + + @Test + @DisplayName("post와 get은 달라야한다.") + void getAndPost() { + String url = "/index.html"; + ControllerKey key1 = new ControllerKey(HttpMethod.GET, url); + ControllerKey key2 = new ControllerKey(HttpMethod.POST, url); + + softly.assertThat(key1).isNotEqualTo(key2); + softly.assertThat(key1.hashCode()).isNotEqualTo(key2.hashCode()); + } +} \ No newline at end of file diff --git a/src/test/java/webserver/HttpRequestTest.java b/src/test/java/http/HttpRequestTest.java similarity index 93% rename from src/test/java/webserver/HttpRequestTest.java rename to src/test/java/http/HttpRequestTest.java index f3b6cbb..bceb82c 100644 --- a/src/test/java/webserver/HttpRequestTest.java +++ b/src/test/java/http/HttpRequestTest.java @@ -1,4 +1,4 @@ -package webserver; +package http; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.AfterEach; @@ -24,7 +24,7 @@ public void afterEach() { @Test @DisplayName("리퀘스트 객체를 생성") - public void createRequest() { + public void createRequest() throws IOException { String request = "GET /index.html HTTP/1.1\r\n" + "Host: localhost:8080\r\n" + "Connection: keep-alive\r\n" + @@ -36,7 +36,7 @@ public void createRequest() { @Test @DisplayName("리퀘스트 쿼리 분석") - public void requestQuery() { + public void requestQuery() throws IOException { String request = "GET /user/create?userId=javajigi&password=password&name=%EB%B0%95%EC%9E%AC%EC%84%B1&email=javajigi%40slipp.net HTTP/1.1\r\n" + "Host: localhost:8080\r\n" + "Connection: keep-alive\r\n" + diff --git a/src/test/java/http/HttpResponseTest.java b/src/test/java/http/HttpResponseTest.java new file mode 100644 index 0000000..3d38090 --- /dev/null +++ b/src/test/java/http/HttpResponseTest.java @@ -0,0 +1,88 @@ +package http; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.assertj.core.api.SoftAssertions; + +class HttpResponseTest { + + private SoftAssertions softly; + private ByteArrayOutputStream os; + private HttpResponse response; + + @BeforeEach + public void beforeEach() { + softly = new SoftAssertions(); + os = new ByteArrayOutputStream(); + response = new HttpResponse(os); + } + + @AfterEach + public void afterEach() { + softly.assertAll(); + } + + @Test + @DisplayName("index.html 읽기") + public void sendOk() throws IOException { + response.forward("/index.html"); + + assertResponseHeader("/index.html", "html"); + softly.assertThat(os.toString()).contains("SLiPP Java Web Programming"); + } + + @Test + @DisplayName("css 파일 읽기") + public void sendOkCss() throws IOException { + response.forward("/css/styles.css"); + + assertResponseHeader("/css/styles.css", "css"); + softly.assertThat(os.toString()).contains(".navbar-default .dropdown-menu li > a {padding-left:30px;}"); + } + + @Test + @DisplayName("redirect to /index.html") + public void sendRedirect() throws IOException { + String url = "/index.html"; + response.redirect(url); + + assertRedirectResponseHeader(url); + } + + @Test + @DisplayName("redirect cookie test") + public void sendRedirectWithCookie() throws IOException { + String url = "/index.html"; + response.addHeader("Set-Cookie", "logined=true"); + response.redirect(url); + + assertRedirectResponseHeader(url); + softly.assertThat(os.toString()) + .contains("Set-Cookie: logined=true"); + } + + private void assertRedirectResponseHeader(String url) { + softly.assertThat(os.toString()) + .contains("HTTP/1.1 302 FOUND \r\n") + .contains("Location: " + url + "\r\n") + .contains("\r\n\r\n"); + } + + private void assertResponseHeader(String url, String type) throws IOException { + byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath()); + + softly.assertThat(os.toString()) + .contains("HTTP/1.1 200 OK \r\n") + .contains("Content-Type: text/" + type + ";charset=utf-8\r\n") + .contains("Content-Length: " + body.length + "\r\n"); + } + +}