diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f63e4a797..32019de097 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: branches: - main pull_request: - types: [opened, synchronize, reopened] + types: [ opened, synchronize, reopened ] jobs: build: name: Build and analyze @@ -34,4 +34,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew clean build codeCoverageReport --info -x :study:build + run: ./gradlew clean build codeCoverageReport sonar --info -x :study:build diff --git a/app/build.gradle b/app/build.gradle index b96dc81587..ef937a5039 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,6 +26,8 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2" testImplementation "org.mockito:mockito-core:5.4.0" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.2" + + testImplementation "org.apache.httpcomponents:httpclient:4.5.13" } idea { diff --git a/app/src/main/java/com/techcourse/DispatcherServlet.java b/app/src/main/java/com/techcourse/DispatcherServlet.java index 277d8eed9a..c7579414cf 100644 --- a/app/src/main/java/com/techcourse/DispatcherServlet.java +++ b/app/src/main/java/com/techcourse/DispatcherServlet.java @@ -1,51 +1,63 @@ package com.techcourse; +import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import webmvc.org.springframework.web.servlet.view.JspView; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.View; +import webmvc.org.springframework.web.servlet.mvc.tobe.exception.HandlerAdapterNotExistException; +import webmvc.org.springframework.web.servlet.mvc.tobe.exception.HandlerNotExistException; +import webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter.AnnotationHandlerAdapter; +import webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter.ControllerHandlerAdapter; +import webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter.HandlerAdapters; +import webmvc.org.springframework.web.servlet.mvc.tobe.handlermapping.AnnotationHandlerMapping; +import webmvc.org.springframework.web.servlet.mvc.tobe.handlermapping.HandlerMappings; public class DispatcherServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class); - private ManualHandlerMapping manualHandlerMapping; - - public DispatcherServlet() { - } + private HandlerMappings handlerMappings = new HandlerMappings(); + private HandlerAdapters handlerAdapters = new HandlerAdapters(); @Override public void init() { - manualHandlerMapping = new ManualHandlerMapping(); - manualHandlerMapping.initialize(); + final String packageName = getClass().getPackageName(); + handlerMappings.addHandlerMapping(new ManualHandlerMapping()); + handlerMappings.addHandlerMapping(new AnnotationHandlerMapping(packageName)); + handlerAdapters.addAdapter(new ControllerHandlerAdapter()); + handlerAdapters.addAdapter(new AnnotationHandlerAdapter()); + handlerMappings.initialize(); } @Override - protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException { + protected void service(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { final String requestURI = request.getRequestURI(); log.debug("Method : {}, Request URI : {}", request.getMethod(), requestURI); - try { - final var controller = manualHandlerMapping.getHandler(requestURI); - final var viewName = controller.execute(request, response); - move(viewName, request, response); - } catch (Throwable e) { - log.error("Exception : {}", e.getMessage(), e); + final Object handler = handlerMappings.getHandler(request); + final ModelAndView modelAndView = handlerAdapters.handle(handler, request, response); + final View view = modelAndView.getView(); + view.render(Collections.emptyMap(), request, response); + } catch (HandlerNotExistException | HandlerAdapterNotExistException exception) { + setNotFound(request, response); + } catch (Exception e) { throw new ServletException(e.getMessage()); } } - private void move(final String viewName, final HttpServletRequest request, final HttpServletResponse response) throws Exception { - if (viewName.startsWith(JspView.REDIRECT_PREFIX)) { - response.sendRedirect(viewName.substring(JspView.REDIRECT_PREFIX.length())); - return; - } - - final var requestDispatcher = request.getRequestDispatcher(viewName); + private void setNotFound(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + final RequestDispatcher requestDispatcher = request.getRequestDispatcher("404.jsp"); + response.setStatus(404); requestDispatcher.forward(request, response); } } diff --git a/app/src/main/java/com/techcourse/ManualHandlerMapping.java b/app/src/main/java/com/techcourse/ManualHandlerMapping.java index a54863caf8..8471cccdab 100644 --- a/app/src/main/java/com/techcourse/ManualHandlerMapping.java +++ b/app/src/main/java/com/techcourse/ManualHandlerMapping.java @@ -1,35 +1,47 @@ package com.techcourse; -import com.techcourse.controller.*; +import com.techcourse.controller.LoginController; +import com.techcourse.controller.LoginViewController; +import com.techcourse.controller.LogoutController; +import com.techcourse.controller.RegisterViewController; +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import webmvc.org.springframework.web.servlet.mvc.asis.Controller; import webmvc.org.springframework.web.servlet.mvc.asis.ForwardController; +import webmvc.org.springframework.web.servlet.mvc.tobe.handlermapping.HandlerMapping; -import java.util.HashMap; -import java.util.Map; - -public class ManualHandlerMapping { +public class ManualHandlerMapping implements HandlerMapping { private static final Logger log = LoggerFactory.getLogger(ManualHandlerMapping.class); private static final Map controllers = new HashMap<>(); + @Override public void initialize() { controllers.put("/", new ForwardController("/index.jsp")); controllers.put("/login", new LoginController()); controllers.put("/login/view", new LoginViewController()); controllers.put("/logout", new LogoutController()); controllers.put("/register/view", new RegisterViewController()); - controllers.put("/register", new RegisterController()); log.info("Initialized Handler Mapping!"); controllers.keySet() - .forEach(path -> log.info("Path : {}, Controller : {}", path, controllers.get(path).getClass())); + .forEach(path -> log.info("Path : {}, Controller : {}", path, + controllers.get(path).getClass())); } - public Controller getHandler(final String requestURI) { + @Override + public Optional getHandler(final HttpServletRequest request) { + final String requestURI = request.getRequestURI(); log.debug("Request Mapping Uri : {}", requestURI); - return controllers.get(requestURI); + + if (controllers.containsKey(requestURI)) { + return Optional.of(controllers.get(requestURI)); + } + return Optional.empty(); } } diff --git a/app/src/main/java/com/techcourse/controller/RegisterController.java b/app/src/main/java/com/techcourse/controller/RegisterController.java index da62e5e8e9..f0b2273784 100644 --- a/app/src/main/java/com/techcourse/controller/RegisterController.java +++ b/app/src/main/java/com/techcourse/controller/RegisterController.java @@ -2,20 +2,38 @@ import com.techcourse.domain.User; import com.techcourse.repository.InMemoryUserRepository; +import context.org.springframework.stereotype.Controller; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import webmvc.org.springframework.web.servlet.mvc.asis.Controller; +import web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.view.JspView; -public class RegisterController implements Controller { +@Controller +public class RegisterController { - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { - final var user = new User(2, - req.getParameter("account"), - req.getParameter("password"), - req.getParameter("email")); - InMemoryUserRepository.save(user); + @RequestMapping(value = "/register", method = RequestMethod.GET) + public ModelAndView show(final HttpServletRequest req, final HttpServletResponse res) { + final String redirectViewName = "redirect:register.jsp"; + + return new ModelAndView(new JspView(redirectViewName)); + } + + @RequestMapping(value = "/register", method = RequestMethod.POST) + public ModelAndView registerUser(final HttpServletRequest req, final HttpServletResponse res) { + final String redirectViewName = "redirect:/index.jsp"; + saveUser(req); - return "redirect:/index.jsp"; + return new ModelAndView(new JspView(redirectViewName)); + } + + private void saveUser(final HttpServletRequest request) { + final var user = new User( + request.getParameter("account"), + request.getParameter("password"), + request.getParameter("email") + ); + InMemoryUserRepository.save(user); } } diff --git a/app/src/main/java/com/techcourse/domain/User.java b/app/src/main/java/com/techcourse/domain/User.java index beb0919b7e..6510b30e06 100644 --- a/app/src/main/java/com/techcourse/domain/User.java +++ b/app/src/main/java/com/techcourse/domain/User.java @@ -2,18 +2,21 @@ public class User { - private final long id; + private long id; private final String account; private final String password; private final String email; - public User(long id, String account, String password, String email) { - this.id = id; + public User(final String account, final String password, final String email) { this.account = account; this.password = password; this.email = email; } + public void setId(final long id) { + this.id = id; + } + public boolean checkPassword(String password) { return this.password.equals(password); } @@ -25,10 +28,10 @@ public String getAccount() { @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/app/src/main/java/com/techcourse/repository/InMemoryUserRepository.java b/app/src/main/java/com/techcourse/repository/InMemoryUserRepository.java index 59bec6d8a8..9482b0535d 100644 --- a/app/src/main/java/com/techcourse/repository/InMemoryUserRepository.java +++ b/app/src/main/java/com/techcourse/repository/InMemoryUserRepository.java @@ -1,27 +1,30 @@ package com.techcourse.repository; import com.techcourse.domain.User; - import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; public class InMemoryUserRepository { - private static final Map database = new ConcurrentHashMap<>(); + private static final AtomicLong RESERVED_ID = new AtomicLong(0); + + public static final Map database = new ConcurrentHashMap<>(); static { - final var user = new User(1, "gugu", "password", "hkkang@woowahan.com"); - database.put(user.getAccount(), user); + save(new User("gugu", "password", "hkkang@woowahan.com")); } - public static void save(User user) { + public static void save(final User user) { + user.setId(RESERVED_ID.incrementAndGet()); database.put(user.getAccount(), user); } - public static Optional findByAccount(String account) { + public static Optional findByAccount(final String account) { return Optional.ofNullable(database.get(account)); } - private InMemoryUserRepository() {} + private InMemoryUserRepository() { + } } diff --git a/app/src/test/java/com/techcourse/controller/LoginControllerTest.java b/app/src/test/java/com/techcourse/controller/LoginControllerTest.java new file mode 100644 index 0000000000..37ef226f41 --- /dev/null +++ b/app/src/test/java/com/techcourse/controller/LoginControllerTest.java @@ -0,0 +1,77 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import org.apache.http.HttpResponse; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("/login 경로 요청 테스트") +class LoginControllerTest extends UsingTomcatTest { + + @DisplayName("POST로 로그인 정보를 form-data 형식으로 본문에 담아 요청시 유저가 있을 경우, 302 응답 코드와 Location 헤더에 /index.jsp 를 담아 응답한다.") + @Test + void login_success() { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + //given + final HttpPost httpPost = new HttpPost(tomcatUrl + "/login"); + + final List formData = new ArrayList<>(); + formData.add(new BasicNameValuePair("account", "gugu")); + formData.add(new BasicNameValuePair("password", "password")); + + final UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formData); + httpPost.setEntity(urlEncodedFormEntity); + + //when + final HttpResponse response = httpClient.execute(httpPost); + + //then + final int actualStatusCode = response.getStatusLine().getStatusCode(); + final String actualLocationHeaderValue = response.getFirstHeader("Location").getValue(); + + assertThat(actualStatusCode).isEqualTo(302); + assertThat(actualLocationHeaderValue).isEqualTo("/index.jsp"); + } catch (Exception e) { + Assertions.fail(); + e.printStackTrace(); + } + } + + @DisplayName("POST로 로그인 정보를 form-data 형식으로 본문에 담아 요청시 유저가 없을 경우, 302 응답 코드와 Location 헤더에 /401.jsp 를 담아 응답한다.") + @Test + void login_fail() { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + //given + final HttpPost httpPost = new HttpPost(tomcatUrl + "/login"); + + final List formData = new ArrayList<>(); + formData.add(new BasicNameValuePair("account", "gugu")); + formData.add(new BasicNameValuePair("password", "wrong")); + + final UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formData); + httpPost.setEntity(urlEncodedFormEntity); + + //when + final HttpResponse response = httpClient.execute(httpPost); + + //then + final int actualStatusCode = response.getStatusLine().getStatusCode(); + final String actualLocationHeaderValue = response.getFirstHeader("Location").getValue(); + + assertThat(actualStatusCode).isEqualTo(302); + assertThat(actualLocationHeaderValue).isEqualTo("/401.jsp"); + } catch (Exception e) { + Assertions.fail(); + e.printStackTrace(); + } + } +} diff --git a/app/src/test/java/com/techcourse/controller/LoginViewControllerTest.java b/app/src/test/java/com/techcourse/controller/LoginViewControllerTest.java new file mode 100644 index 0000000000..3577d77b7b --- /dev/null +++ b/app/src/test/java/com/techcourse/controller/LoginViewControllerTest.java @@ -0,0 +1,37 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("/login/view 경로 요청 테스트") +class LoginViewControllerTest extends UsingTomcatTest { + + @DisplayName("GET 요청시 200 상태코드 와 함께 로그인 페이지로 응답한다.") + @Test + void save() { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + //given + final HttpPost httpPost = new HttpPost(tomcatUrl + "/login/view"); + + //when + final HttpResponse response = httpClient.execute(httpPost); + + //then + final int actualStatusCode = response.getStatusLine().getStatusCode(); + final Header contentTypeHeader = response.getFirstHeader("Content-Type"); + assertThat(actualStatusCode).isEqualTo(200); + assertThat(contentTypeHeader.getValue()).isEqualTo("text/html;charset=UTF-8"); + } catch (Exception e) { + Assertions.fail(); + e.printStackTrace(); + } + } +} diff --git a/app/src/test/java/com/techcourse/controller/LogoutControllerTest.java b/app/src/test/java/com/techcourse/controller/LogoutControllerTest.java new file mode 100644 index 0000000000..d39c7538ce --- /dev/null +++ b/app/src/test/java/com/techcourse/controller/LogoutControllerTest.java @@ -0,0 +1,36 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class LogoutControllerTest extends UsingTomcatTest { + + @DisplayName("요청시 302 상태코드와 / 로 리다이렉트한다.") + @Test + void save() { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + //given + final HttpPost httpPost = new HttpPost(tomcatUrl + "/logout"); + + //when + final HttpResponse response = httpClient.execute(httpPost); + + //then + final int actualStatusCode = response.getStatusLine().getStatusCode(); + final String actualLocationHeaderValue = response.getFirstHeader("Location").getValue(); + + assertThat(actualStatusCode).isEqualTo(302); + assertThat(actualLocationHeaderValue).isEqualTo("/"); + } catch (Exception e) { + Assertions.fail(); + e.printStackTrace(); + } + } +} diff --git a/app/src/test/java/com/techcourse/controller/RegisterControllerTest.java b/app/src/test/java/com/techcourse/controller/RegisterControllerTest.java new file mode 100644 index 0000000000..0290d675db --- /dev/null +++ b/app/src/test/java/com/techcourse/controller/RegisterControllerTest.java @@ -0,0 +1,76 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("/register 경로 요청 테스트") +class RegisterControllerTest extends UsingTomcatTest { + + @DisplayName("GET 로 요청을 보내 200 상태코드와 회원가입 페이지를 본문에 담아 반환한다.") + @Test + void show() { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + //given + final HttpGet httpGet = new HttpGet(tomcatUrl + "/register"); + + //when + final HttpResponse response = httpClient.execute(httpGet); + + //then + final int actualStatusCode = response.getStatusLine().getStatusCode(); + final Header contentTypeHeader = response.getFirstHeader("Content-Type"); + + assertThat(actualStatusCode).isEqualTo(200); + assertThat(contentTypeHeader.getValue()).isEqualTo("text/html;charset=UTF-8"); + } catch (Exception e) { + Assertions.fail(); + e.printStackTrace(); + } + } + + @DisplayName("회원가입 정보를 form-data 형식으로 본문에 담아 POST 요청시 유저를 저장하고 302 상태코드, Location 헤더에 /index.html 를 담아 응답한다.") + @Test + void save() { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + //given + final HttpPost httpPost = new HttpPost(tomcatUrl + "/register"); + + final List formData = new ArrayList<>(); + formData.add(new BasicNameValuePair("account", "split")); + formData.add(new BasicNameValuePair("email", "split@daum.net")); + formData.add(new BasicNameValuePair("password", "password")); + + final UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formData); + httpPost.setEntity(urlEncodedFormEntity); + + //when + final HttpResponse response = httpClient.execute(httpPost); + + //then + final String body = EntityUtils.toString(response.getEntity()); + final int actualStatusCode = response.getStatusLine().getStatusCode(); + final String actualLocationHeaderValue = response.getFirstHeader("Location").getValue(); + + assertThat(actualStatusCode).isEqualTo(302); + assertThat(actualLocationHeaderValue).isEqualTo("/index.jsp"); + assertThat(body).isEmpty(); + } catch (Exception e) { + Assertions.fail(); + e.printStackTrace(); + } + } +} diff --git a/app/src/test/java/com/techcourse/controller/RegisterViewControllerTest.java b/app/src/test/java/com/techcourse/controller/RegisterViewControllerTest.java new file mode 100644 index 0000000000..d9834d9235 --- /dev/null +++ b/app/src/test/java/com/techcourse/controller/RegisterViewControllerTest.java @@ -0,0 +1,36 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RegisterViewControllerTest extends UsingTomcatTest { + + @DisplayName("요청시 200 상태코드 와 함께 회원가입 페이지를 담아 응답한다.") + @Test + void save() { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + //given + final HttpPost httpPost = new HttpPost(tomcatUrl + "/register/view"); + + //when + final HttpResponse response = httpClient.execute(httpPost); + + //then + final int actualStatusCode = response.getStatusLine().getStatusCode(); + final Header contentTypeHeader = response.getFirstHeader("Content-Type"); + assertThat(actualStatusCode).isEqualTo(200); + assertThat(contentTypeHeader.getValue()).isEqualTo("text/html;charset=UTF-8"); + } catch (Exception e) { + Assertions.fail(); + e.printStackTrace(); + } + } +} diff --git a/app/src/test/java/com/techcourse/controller/UsingTomcatTest.java b/app/src/test/java/com/techcourse/controller/UsingTomcatTest.java new file mode 100644 index 0000000000..5ef2e75639 --- /dev/null +++ b/app/src/test/java/com/techcourse/controller/UsingTomcatTest.java @@ -0,0 +1,34 @@ +package com.techcourse.controller; + +import com.techcourse.TomcatStarter; +import java.io.IOException; +import java.net.ServerSocket; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; + +public abstract class UsingTomcatTest { + + protected static String tomcatUrl; + protected static TomcatStarter tomcatStarter; + + public static int getLocalPort() { + try (final ServerSocket serverSocket = new ServerSocket(0)) { + return serverSocket.getLocalPort(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @BeforeAll + static void setUp() { + final int port = getLocalPort(); + tomcatStarter = new TomcatStarter("src/main/webapp/", port); + tomcatUrl = "http://localhost:" + port; + tomcatStarter.start(); + } + + @AfterAll + static void cleanUp() { + tomcatStarter.stop(); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java deleted file mode 100644 index 6b14960f62..0000000000 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java +++ /dev/null @@ -1,24 +0,0 @@ -package webmvc.org.springframework.web.servlet.mvc.tobe; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import webmvc.org.springframework.web.servlet.ModelAndView; - -public class HandlerExecution { - - private final Object handler; - private final Method method; - - public HandlerExecution(final Object handler, final Method method) { - this.handler = handler; - this.method = method; - } - - public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) - throws InvocationTargetException, IllegalAccessException { - - return (ModelAndView) method.invoke(handler, request, response); - } -} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/exception/HandlerAdapterNotExistException.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/exception/HandlerAdapterNotExistException.java new file mode 100644 index 0000000000..fbe00cad24 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/exception/HandlerAdapterNotExistException.java @@ -0,0 +1,8 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe.exception; + +public class HandlerAdapterNotExistException extends RuntimeException { + + public HandlerAdapterNotExistException() { + super(); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/exception/HandlerNotExistException.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/exception/HandlerNotExistException.java new file mode 100644 index 0000000000..7f226c9d93 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/exception/HandlerNotExistException.java @@ -0,0 +1,8 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe.exception; + +public class HandlerNotExistException extends RuntimeException { + + public HandlerNotExistException() { + super(); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/exception/InvalidHandlerForHandlerAdapterException.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/exception/InvalidHandlerForHandlerAdapterException.java new file mode 100644 index 0000000000..9b7ed412f9 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/exception/InvalidHandlerForHandlerAdapterException.java @@ -0,0 +1,16 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe.exception; + +import webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter.HandlerAdapter; + +public class InvalidHandlerForHandlerAdapterException extends RuntimeException { + + public InvalidHandlerForHandlerAdapterException(final Object handler, + final HandlerAdapter adapter) { + super(String.format( + "Adapter can not handle handler. ( Adapter : %s, Handler : %s )", + handler.toString(), + adapter.toString() + ) + ); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/AnnotationHandlerAdapter.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/AnnotationHandlerAdapter.java new file mode 100644 index 0000000000..22c22c49c8 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/AnnotationHandlerAdapter.java @@ -0,0 +1,23 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.mvc.tobe.exception.InvalidHandlerForHandlerAdapterException; + +public class AnnotationHandlerAdapter implements HandlerAdapter { + + @Override + public boolean isHandleable(final Object handler) { + return handler instanceof HandlerExecution; + } + + @Override + public ModelAndView handle(final Object handler, final HttpServletRequest request, + final HttpServletResponse response) throws Exception { + if (!isHandleable(handler)) { + throw new InvalidHandlerForHandlerAdapterException(handler, this); + } + return ((HandlerExecution) handler).handle(request, response); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/ControllerHandlerAdapter.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/ControllerHandlerAdapter.java new file mode 100644 index 0000000000..7841768150 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/ControllerHandlerAdapter.java @@ -0,0 +1,27 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.mvc.asis.Controller; +import webmvc.org.springframework.web.servlet.mvc.tobe.exception.InvalidHandlerForHandlerAdapterException; +import webmvc.org.springframework.web.servlet.view.JspView; + +public class ControllerHandlerAdapter implements HandlerAdapter { + + @Override + public boolean isHandleable(final Object handler) { + return handler instanceof Controller; + } + + @Override + public ModelAndView handle(final Object handler, final HttpServletRequest request, + final HttpServletResponse response) throws Exception { + if (!isHandleable(handler)) { + throw new InvalidHandlerForHandlerAdapterException(handler, this); + } + final String viewName = ((Controller) handler).execute(request, response); + final JspView jspView = new JspView(viewName); + return new ModelAndView(jspView); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/HandlerAdapter.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/HandlerAdapter.java new file mode 100644 index 0000000000..c7b26a4824 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/HandlerAdapter.java @@ -0,0 +1,13 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.ModelAndView; + +public interface HandlerAdapter { + + boolean isHandleable(final Object handler); + + ModelAndView handle(final Object handler, final HttpServletRequest request, final + HttpServletResponse response) throws Exception; +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/HandlerAdapters.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/HandlerAdapters.java new file mode 100644 index 0000000000..d29d825a48 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/HandlerAdapters.java @@ -0,0 +1,27 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.mvc.tobe.exception.HandlerAdapterNotExistException; + +public class HandlerAdapters { + + private final List adapters = new ArrayList<>(); + + public void addAdapter(final HandlerAdapter handlerAdapter) { + adapters.add(handlerAdapter); + } + + public ModelAndView handle(final Object handler, final HttpServletRequest request, + final HttpServletResponse response) throws Exception { + for (HandlerAdapter adapter : adapters) { + if (adapter.isHandleable(handler)) { + return adapter.handle(handler, request, response); + } + } + throw new HandlerAdapterNotExistException(); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/HandlerExecution.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/HandlerExecution.java new file mode 100644 index 0000000000..87d5280458 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/HandlerExecution.java @@ -0,0 +1,45 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import webmvc.org.springframework.web.servlet.ModelAndView; + +public class HandlerExecution { + + private final Object handler; + private final Method handlerMethod; + + public HandlerExecution(final Object handler, final Method handlerMethod) { + validateDeclare(handler, handlerMethod); + validateHandlerMethod(handlerMethod); + this.handler = handler; + this.handlerMethod = handlerMethod; + } + + public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) + throws InvocationTargetException, IllegalAccessException { + + return (ModelAndView) handlerMethod.invoke(handler, request, response); + } + + private void validateDeclare(final Object handler, final Method handlerMethod) { + final Class handlerClass = handler.getClass(); + final Class declaringClass = handlerMethod.getDeclaringClass(); + if (!handlerClass.equals(declaringClass)) { + throw new IllegalArgumentException("클래스에 해당 메서드가 없습니다."); + } + } + + private void validateHandlerMethod(final Method handlerMethod) { + final List> parameterTypes = Arrays.asList(handlerMethod.getParameterTypes()); + if (parameterTypes.contains(HttpServletRequest.class) && + parameterTypes.contains(HttpServletResponse.class)) { + return; + } + throw new IllegalArgumentException("메서드 인자가 적절하지 않습니다. parameters : " + parameterTypes); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKey.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/HandlerKey.java similarity index 69% rename from mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKey.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/HandlerKey.java index 30d3c780ff..0dc1ac7288 100644 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKey.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handleradapter/HandlerKey.java @@ -1,8 +1,7 @@ -package webmvc.org.springframework.web.servlet.mvc.tobe; - -import web.org.springframework.web.bind.annotation.RequestMethod; +package webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter; import java.util.Objects; +import web.org.springframework.web.bind.annotation.RequestMethod; public class HandlerKey { @@ -17,15 +16,19 @@ public HandlerKey(final String url, final RequestMethod requestMethod) { @Override public String toString() { return "HandlerKey{" + - "url='" + url + '\'' + - ", requestMethod=" + requestMethod + - '}'; + "url='" + url + '\'' + + ", requestMethod=" + requestMethod + + '}'; } @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof HandlerKey)) return false; + if (this == o) { + return true; + } + if (!(o instanceof HandlerKey)) { + return false; + } HandlerKey that = (HandlerKey) o; return Objects.equals(url, that.url) && requestMethod == that.requestMethod; } diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handlermapping/AnnotationHandlerMapping.java similarity index 73% rename from mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handlermapping/AnnotationHandlerMapping.java index cc0d300083..f9203d0622 100644 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handlermapping/AnnotationHandlerMapping.java @@ -1,27 +1,27 @@ -package webmvc.org.springframework.web.servlet.mvc.tobe; +package webmvc.org.springframework.web.servlet.mvc.tobe.handlermapping; import context.org.springframework.stereotype.Controller; import jakarta.servlet.http.HttpServletRequest; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.reflections.Reflections; -import org.reflections.scanners.Scanners; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import web.org.springframework.web.bind.annotation.RequestMapping; import web.org.springframework.web.bind.annotation.RequestMethod; -import webmvc.org.springframework.web.servlet.mvc.tobe.exception.ControllerClassNotFoundByNameException; import webmvc.org.springframework.web.servlet.mvc.tobe.exception.ReflectionInstantiationException; +import webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter.HandlerExecution; +import webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter.HandlerKey; -public class AnnotationHandlerMapping { +public class AnnotationHandlerMapping implements HandlerMapping { private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class); @@ -33,6 +33,7 @@ public AnnotationHandlerMapping(final Object... basePackage) { this.handlerExecutions = new HashMap<>(); } + @Override public void initialize() { for (final Object packageName : basePackage) { addHandlerExecutionsScannedInPackage(packageName); @@ -42,39 +43,30 @@ public void initialize() { } private void addHandlerExecutionsScannedInPackage(final Object packageName) { - final List> controllerClasses = getControllerClasses(packageName); + final Set> controllerClasses = getControllerClasses(packageName); for (Class controllerClass : controllerClasses) { addHandlerExecutionsInController(controllerClass); } } - private List> getControllerClasses(final Object name) { + private Set> getControllerClasses(final Object name) { final Reflections reflections = new Reflections(name); - final Set controllerNames = - reflections.get(Scanners.TypesAnnotated.with(Controller.class)); - - final List> controllerClasses = new ArrayList<>(); - for (String controllerName : controllerNames) { - try { - controllerClasses.add(Class.forName(controllerName)); - } catch (ClassNotFoundException e) { - throw new ControllerClassNotFoundByNameException(controllerName); - } - } - - return controllerClasses; + return reflections.getTypesAnnotatedWith(Controller.class); } private void addHandlerExecutionsInController(final Class controllerClass) { final List requestMappingMethods = getRequestMappingMethods(controllerClass); + final Object controller = instantiateClass(controllerClass); + for (Method requestMappingMethod : requestMappingMethods) { + addHandlerExecutions(controller, requestMappingMethod); + } + } + + private Object instantiateClass(final Class classType) { try { - final Object controller = controllerClass.getDeclaredConstructor() + return classType.getDeclaredConstructor() .newInstance(); - - for (Method requestMappingMethod : requestMappingMethods) { - addHandlerExecutions(controller, requestMappingMethod); - } } catch (InstantiationException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { throw new ReflectionInstantiationException("controller 인스턴스 생성에 실패하였습니다."); @@ -110,12 +102,15 @@ private void addHandlerExecutions(final Object controller, final Method method) } } - public Object getHandler(final HttpServletRequest request) { + @Override + public Optional getHandler(final HttpServletRequest request) { final HandlerKey handlerKey = new HandlerKey( request.getRequestURI(), RequestMethod.valueOf(request.getMethod()) ); - - return handlerExecutions.get(handlerKey); + if (handlerExecutions.containsKey(handlerKey)) { + return Optional.of(handlerExecutions.get(handlerKey)); + } + return Optional.empty(); } } diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handlermapping/HandlerMapping.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handlermapping/HandlerMapping.java new file mode 100644 index 0000000000..a3fcfc422b --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handlermapping/HandlerMapping.java @@ -0,0 +1,12 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe.handlermapping; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Optional; + +public interface HandlerMapping { + + void initialize(); + + Optional getHandler(final HttpServletRequest request); + +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handlermapping/HandlerMappings.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handlermapping/HandlerMappings.java new file mode 100644 index 0000000000..a8817e397b --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/handlermapping/HandlerMappings.java @@ -0,0 +1,32 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe.handlermapping; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import webmvc.org.springframework.web.servlet.mvc.tobe.exception.HandlerNotExistException; + +public class HandlerMappings { + + private List handlerMappings = new ArrayList<>(); + + public void addHandlerMapping(final HandlerMapping handlerMapping) { + handlerMappings.add(handlerMapping); + } + + public void initialize() { + for (HandlerMapping handlerMapping : handlerMappings) { + handlerMapping.initialize(); + } + } + + public Object getHandler(final HttpServletRequest request) { + for (HandlerMapping handlerMapping : handlerMappings) { + final Optional handler = handlerMapping.getHandler(request); + if (handler.isPresent()) { + return handler.get(); + } + } + throw new HandlerNotExistException(); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java index b42c3466f0..6d6be91fc1 100644 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java @@ -2,13 +2,13 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import webmvc.org.springframework.web.servlet.View; - import java.util.Map; +import webmvc.org.springframework.web.servlet.View; public class JsonView implements View { @Override - public void render(final Map model, final HttpServletRequest request, HttpServletResponse response) throws Exception { + public void render(final Map model, final HttpServletRequest request, + HttpServletResponse response) throws Exception { } } diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java index 3f4cc906ff..4e44760aa3 100644 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java @@ -1,31 +1,38 @@ package webmvc.org.springframework.web.servlet.view; +import jakarta.servlet.RequestDispatcher; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import webmvc.org.springframework.web.servlet.View; -import java.util.Map; - public class JspView implements View { private static final Logger log = LoggerFactory.getLogger(JspView.class); public static final String REDIRECT_PREFIX = "redirect:"; + private final String viewName; public JspView(final String viewName) { + this.viewName = viewName; } @Override - public void render(final Map model, final HttpServletRequest request, final HttpServletResponse response) throws Exception { - // todo + public void render(final Map model, final HttpServletRequest request, + final HttpServletResponse response) throws Exception { + if (viewName.startsWith(JspView.REDIRECT_PREFIX)) { + response.sendRedirect(viewName.substring(JspView.REDIRECT_PREFIX.length())); + return; + } model.keySet().forEach(key -> { log.debug("attribute name : {}, value : {}", key, model.get(key)); request.setAttribute(key, model.get(key)); }); - // todo + final RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewName); + requestDispatcher.forward(request, response); } } diff --git a/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index dcec215a3f..a814ab96a4 100644 --- a/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -1,13 +1,15 @@ package webmvc.org.springframework.web.servlet.mvc.tobe; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter.HandlerExecution; +import webmvc.org.springframework.web.servlet.mvc.tobe.handlermapping.AnnotationHandlerMapping; class AnnotationHandlerMappingTest { @@ -28,7 +30,7 @@ void get() throws Exception { when(request.getRequestURI()).thenReturn("/get-test"); when(request.getMethod()).thenReturn("GET"); - final var handlerExecution = (HandlerExecution) handlerMapping.getHandler(request); + final var handlerExecution = (HandlerExecution) handlerMapping.getHandler(request).get(); final var modelAndView = handlerExecution.handle(request, response); assertThat(modelAndView.getObject("id")).isEqualTo("gugu"); @@ -43,7 +45,7 @@ void post() throws Exception { when(request.getRequestURI()).thenReturn("/post-test"); when(request.getMethod()).thenReturn("POST"); - final var handlerExecution = (HandlerExecution) handlerMapping.getHandler(request); + final var handlerExecution = (HandlerExecution) handlerMapping.getHandler(request).get(); final var modelAndView = handlerExecution.handle(request, response); assertThat(modelAndView.getObject("id")).isEqualTo("gugu");