diff --git a/app/src/main/java/com/techcourse/DispatcherServletInitializer.java b/app/src/main/java/com/techcourse/DispatcherServletInitializer.java index cee825f502..fde4af32d1 100644 --- a/app/src/main/java/com/techcourse/DispatcherServletInitializer.java +++ b/app/src/main/java/com/techcourse/DispatcherServletInitializer.java @@ -1,6 +1,7 @@ package com.techcourse; import com.interface21.web.WebApplicationInitializer; +import com.interface21.webmvc.servlet.DispatcherServlet; import jakarta.servlet.ServletContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,12 +14,12 @@ public class DispatcherServletInitializer implements WebApplicationInitializer { private static final Logger log = LoggerFactory.getLogger(DispatcherServletInitializer.class); + private static final String SCANNING_CONTROLLER_PACKAGE = "com.techcourse.controller"; private static final String DEFAULT_SERVLET_NAME = "dispatcher"; @Override public void onStartup(final ServletContext servletContext) { - final var dispatcherServlet = new DispatcherServlet(); - dispatcherServlet.addHandlerMapping(new ManualHandlerMapping()); + final var dispatcherServlet = new DispatcherServlet(SCANNING_CONTROLLER_PACKAGE); final var registration = servletContext.addServlet(DEFAULT_SERVLET_NAME, dispatcherServlet); if (registration == null) { diff --git a/app/src/main/java/com/techcourse/ManualHandlerMapping.java b/app/src/main/java/com/techcourse/ManualHandlerMapping.java deleted file mode 100644 index a9a6c3cff1..0000000000 --- a/app/src/main/java/com/techcourse/ManualHandlerMapping.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.techcourse; - -import com.interface21.webmvc.servlet.mvc.asis.Controller; -import com.interface21.webmvc.servlet.mvc.asis.ForwardController; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerMapping; -import com.techcourse.controller.LoginController; -import com.techcourse.controller.LoginViewController; -import com.techcourse.controller.LogoutController; -import jakarta.servlet.http.HttpServletRequest; -import java.util.HashMap; -import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ManualHandlerMapping implements HandlerMapping { - - private static final Logger log = LoggerFactory.getLogger(ManualHandlerMapping.class); - - private static final Map controllers = new HashMap<>(); - - 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()); - - log.info("Initialized ManualHandlerMapping!"); - controllers.forEach((path, controller) -> log.info("Path : {}, Controller : {}", path, controller.getClass())); - } - - @Override - public Object getHandler(HttpServletRequest request) { - String requestURI = request.getRequestURI(); - log.debug("Request Mapping Uri : {}", requestURI); - return controllers.get(requestURI); - } -} diff --git a/app/src/main/java/com/techcourse/controller/HomeController.java b/app/src/main/java/com/techcourse/controller/HomeController.java new file mode 100644 index 0000000000..357c5febf5 --- /dev/null +++ b/app/src/main/java/com/techcourse/controller/HomeController.java @@ -0,0 +1,17 @@ +package com.techcourse.controller; + +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.view.JspView; + +@Controller +@RequestMapping +public class HomeController { + + @RequestMapping(method = RequestMethod.GET) + public ModelAndView getHome() { + return new ModelAndView(new JspView("/index.jsp")); + } +} diff --git a/app/src/main/java/com/techcourse/controller/LoginController.java b/app/src/main/java/com/techcourse/controller/LoginController.java index a87d996693..697002f620 100644 --- a/app/src/main/java/com/techcourse/controller/LoginController.java +++ b/app/src/main/java/com/techcourse/controller/LoginController.java @@ -1,29 +1,35 @@ package com.techcourse.controller; +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.view.JspView; import com.techcourse.domain.User; import com.techcourse.repository.InMemoryUserRepository; import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import com.interface21.webmvc.servlet.mvc.asis.Controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class LoginController implements Controller { +@Controller +@RequestMapping("/login") +public class LoginController { private static final Logger log = LoggerFactory.getLogger(LoginController.class); - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { + @RequestMapping(method = RequestMethod.POST) + public ModelAndView postLogin(final HttpServletRequest req) { if (UserSession.isLoggedIn(req.getSession())) { - return "redirect:/index.jsp"; + return new ModelAndView(new JspView("redirect:/index.jsp")); } - return InMemoryUserRepository.findByAccount(req.getParameter("account")) + String viewName = InMemoryUserRepository.findByAccount(req.getParameter("account")) .map(user -> { log.info("User : {}", user); return login(req, user); }) .orElse("redirect:/401.jsp"); + return new ModelAndView(new JspView(viewName)); } private String login(final HttpServletRequest request, final User user) { diff --git a/app/src/main/java/com/techcourse/controller/LoginViewController.java b/app/src/main/java/com/techcourse/controller/LoginViewController.java index dc71ee8e99..0db9f647e6 100644 --- a/app/src/main/java/com/techcourse/controller/LoginViewController.java +++ b/app/src/main/java/com/techcourse/controller/LoginViewController.java @@ -1,22 +1,28 @@ package com.techcourse.controller; +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.view.JspView; import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.interface21.webmvc.servlet.mvc.asis.Controller; -public class LoginViewController implements Controller { +@Controller +@RequestMapping("/login/view") +public class LoginViewController { private static final Logger log = LoggerFactory.getLogger(LoginViewController.class); - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { - return UserSession.getUserFrom(req.getSession()) + @RequestMapping(method = RequestMethod.GET) + public ModelAndView getLoginView(final HttpServletRequest req) throws Exception { + String viewName = UserSession.getUserFrom(req.getSession()) .map(user -> { log.info("logged in {}", user.getAccount()); return "redirect:/index.jsp"; }) .orElse("/login.jsp"); + return new ModelAndView(new JspView(viewName)); } } diff --git a/app/src/main/java/com/techcourse/controller/LogoutController.java b/app/src/main/java/com/techcourse/controller/LogoutController.java index 729d262de5..49a9528405 100644 --- a/app/src/main/java/com/techcourse/controller/LogoutController.java +++ b/app/src/main/java/com/techcourse/controller/LogoutController.java @@ -1,15 +1,20 @@ package com.techcourse.controller; +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.view.JspView; import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import com.interface21.webmvc.servlet.mvc.asis.Controller; -public class LogoutController implements Controller { +@Controller +@RequestMapping("/logout") +public class LogoutController { - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { + @RequestMapping(method = RequestMethod.GET) + public ModelAndView getLogout(final HttpServletRequest req) throws Exception { final var session = req.getSession(); session.removeAttribute(UserSession.SESSION_KEY); - return "redirect:/"; + return new ModelAndView(new JspView("redirect:/")); } } diff --git a/app/src/main/java/com/techcourse/controller/UserController.java b/app/src/main/java/com/techcourse/controller/UserController.java new file mode 100644 index 0000000000..84ffc455f4 --- /dev/null +++ b/app/src/main/java/com/techcourse/controller/UserController.java @@ -0,0 +1,42 @@ +package com.techcourse.controller; + +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.view.JsonView; +import com.techcourse.domain.User; +import com.techcourse.repository.InMemoryUserRepository; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Controller +public class UserController { + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + private static final String PARAM_NAME_ACCOUNT = "account"; + + @RequestMapping(value = "/api/user", method = RequestMethod.GET) + public ModelAndView getUser(HttpServletRequest request, HttpServletResponse response) { + if (!isValidRequest(request)) { + throw new IllegalArgumentException("Please input account as parameter"); + } + + final String account = request.getParameter(PARAM_NAME_ACCOUNT); + log.debug("user id : {}", account); + + final User user = InMemoryUserRepository.findByAccount(account) + .orElseThrow(() -> new IllegalArgumentException("User not found: " + account)); + + final ModelAndView modelAndView = new ModelAndView(new JsonView()); + modelAndView.addObject("user", user); + return modelAndView; + } + + private boolean isValidRequest(HttpServletRequest request) { + String account = request.getParameter(PARAM_NAME_ACCOUNT); + return account != null && !account.isBlank(); + } +} diff --git a/app/src/test/java/com/techcourse/controller/HomeControllerTest.java b/app/src/test/java/com/techcourse/controller/HomeControllerTest.java new file mode 100644 index 0000000000..a8d9925815 --- /dev/null +++ b/app/src/test/java/com/techcourse/controller/HomeControllerTest.java @@ -0,0 +1,49 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.techcourse.TomcatStarter; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +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 HomeControllerTest { + + private static final String URL = "http://localhost:8080"; + + private TomcatStarter tomcat; + + @BeforeEach + void setUp() { + tomcat = new TomcatStarter("src/main/webapp/", 8080); + tomcat.start(); + } + + @AfterEach + void tearDown() { + tomcat.stop(); + } + + @Test + @DisplayName("홈 화면을 요청시 대시보드 페이지를 응답한다.") + void getHome() throws Exception { + // given + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(URL)) + .build(); + + // when + HttpResponse response = client.send(request, BodyHandlers.ofString()); + client.close(); + + // then + assertThat(response.body()).contains("대시보드"); + } +} 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..ec101ff09b --- /dev/null +++ b/app/src/test/java/com/techcourse/controller/LoginControllerTest.java @@ -0,0 +1,104 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.techcourse.TomcatStarter; +import jakarta.servlet.http.HttpServletResponse; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +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 LoginControllerTest { + + private static final String URL = "http://localhost:8080/login"; + private static final String CONTENT_TYPE_URLENCODED = "application/x-www-form-urlencoded"; + private static final String CONTENT_TYPE_NAME = "Content-Type"; + + private TomcatStarter tomcat; + + @BeforeEach + void setUp() { + tomcat = new TomcatStarter("src/main/webapp/", 8080); + tomcat.start(); + } + + @AfterEach + void tearDown() { + tomcat.stop(); + } + + @Test + @DisplayName("올바른 계정 정보로 로그인 요청 시 대시보드 페이지로 리디렉션을 응답한다.") + void postLogin() throws Exception { + // given + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(URL)) + .header(CONTENT_TYPE_NAME, CONTENT_TYPE_URLENCODED) + .POST(BodyPublishers.ofString("account=gugu&password=password")) + .build(); + + // when + HttpResponse response = client.send(request, BodyHandlers.ofString()); + client.close(); + + // then + assertAll( + () -> assertThat(response.headers().firstValue("Location")).hasValue("/index.jsp"), + () -> assertThat(response.statusCode()).isEqualTo(HttpServletResponse.SC_FOUND) + ); + } + + @Test + @DisplayName("존재하지 않는 계정으로 요청 시 401 페이지로 리디렉션을 응답한다.") + void getLoginWithNonAccount() throws Exception { + // given + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(URL)) + .header(CONTENT_TYPE_NAME, CONTENT_TYPE_URLENCODED) + .POST(BodyPublishers.ofString("account=seyang&password=password")) + .build(); + + // when + HttpResponse response = client.send(request, BodyHandlers.ofString()); + client.close(); + + // then + assertAll( + () -> assertThat(response.headers().firstValue("Location")).hasValue("/401.jsp"), + () -> assertThat(response.statusCode()).isEqualTo(HttpServletResponse.SC_FOUND) + ); + } + + @Test + @DisplayName("틀린 비밀번호로 요청 시 401 페이지로 리디렉션을 응답한다.") + void getLoginWithInvalidPassword() throws Exception { + // given + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(URL)) + .header(CONTENT_TYPE_NAME, CONTENT_TYPE_URLENCODED) + .POST(BodyPublishers.ofString("account=gugu&password=pw")) + .build(); + + // when + HttpResponse response = client.send(request, BodyHandlers.ofString()); + response.sslSession(); + client.close(); + + // then + assertAll( + () -> assertThat(response.headers().firstValue("Location")).hasValue("/401.jsp"), + () -> assertThat(response.statusCode()).isEqualTo(HttpServletResponse.SC_FOUND) + ); + } +} diff --git a/app/src/test/java/com/techcourse/controller/UserControllerTest.java b/app/src/test/java/com/techcourse/controller/UserControllerTest.java new file mode 100644 index 0000000000..98224e898f --- /dev/null +++ b/app/src/test/java/com/techcourse/controller/UserControllerTest.java @@ -0,0 +1,91 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.techcourse.TomcatStarter; +import jakarta.servlet.http.HttpServletResponse; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +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 UserControllerTest { + + private static final String URL = "http://localhost:8080/api/user"; + + private TomcatStarter tomcat; + + @BeforeEach + void setUp() { + tomcat = new TomcatStarter("src/main/webapp/", 8080); + tomcat.start(); + } + + @AfterEach + void tearDown() { + tomcat.stop(); + } + + @Test + @DisplayName("올바른 파라미터로 요청 시 유저 정보를 반환한다.") + void getUser() throws Exception { + // given + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(URL + "?account=gugu")) + .build(); + + // when + HttpResponse response = client.send(request, BodyHandlers.ofString()); + client.close(); + + // then + assertThat(response.body()).isEqualTo("{\"user\":{\"account\":\"gugu\"}}"); + } + + @Test + @DisplayName("존재하지 않는 계정 정보를 보내면 400을 반환한다.") + void getUserWithNonExistReturnBadRequest() throws Exception { + // given + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(URL + "?account=seyang")) + .build(); + + // when + HttpResponse response = client.send(request, BodyHandlers.ofString()); + client.close(); + + // then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST), + () -> assertThat(response.body()).isEqualTo("User not found: seyang") + ); + } + + @Test + @DisplayName("빈 계정 정보를 보내면 400을 반환한다.") + void getUserWithEmptyReturnBadRequest() throws Exception { + // given + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(URL + "?account=")) + .build(); + + // when + HttpResponse response = client.send(request, BodyHandlers.ofString()); + client.close(); + + // then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST), + () -> assertThat(response.body()).isEqualTo("Please input account as parameter") + ); + } +} diff --git a/app/src/main/java/com/techcourse/DispatcherServlet.java b/mvc/src/main/java/com/interface21/webmvc/servlet/DispatcherServlet.java similarity index 71% rename from app/src/main/java/com/techcourse/DispatcherServlet.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/DispatcherServlet.java index 4a2c4c46d3..001c5f197a 100644 --- a/app/src/main/java/com/techcourse/DispatcherServlet.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/DispatcherServlet.java @@ -1,11 +1,8 @@ -package com.techcourse; +package com.interface21.webmvc.servlet; -import com.interface21.webmvc.servlet.ModelAndView; -import com.interface21.webmvc.servlet.View; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerAdapter; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerAdapterRegistry; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerMapping; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerMappingRegistry; +import com.interface21.webmvc.servlet.handler.HandlerAdapterRegistry; +import com.interface21.webmvc.servlet.handler.HandlerMappingRegistry; +import com.interface21.webmvc.servlet.resolver.ArgumentHandlerExceptionResolver; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -16,16 +13,17 @@ public class DispatcherServlet extends HttpServlet { - private static final String BASE_PACKAGE = "com.techcourse.controller"; private static final long serialVersionUID = 1L; private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class); private final HandlerAdapterRegistry handlerAdapterRegistry; private final HandlerMappingRegistry handlerMappingRegistry; + private final HandlerExceptionResolver handlerExceptionResolver; - public DispatcherServlet() { + public DispatcherServlet(String basePackage) { handlerAdapterRegistry = new HandlerAdapterRegistry(); - handlerMappingRegistry = new HandlerMappingRegistry(BASE_PACKAGE); + handlerMappingRegistry = new HandlerMappingRegistry(basePackage); + handlerExceptionResolver = new ArgumentHandlerExceptionResolver(); } @Override @@ -40,14 +38,14 @@ protected void service(final HttpServletRequest request, final HttpServletRespon final String requestURI = request.getRequestURI(); log.debug("Method : {}, Request URI : {}", request.getMethod(), requestURI); + Object handler = null; try { - Object handler = findHandler(request); + handler = findHandler(request); HandlerAdapter handlerAdapter = findHandlerAdapter(handler); ModelAndView modelAndView = handlerAdapter.handle(request, response, handler); renderViewWithModel(request, response, modelAndView); - } catch (Throwable e) { - log.error("Exception : {}", e.getMessage(), e); - throw new ServletException(e.getMessage()); + } catch (Exception e) { + handlerExceptionResolver.resolveException(request, response, handler, e); } } @@ -70,8 +68,4 @@ private void renderViewWithModel(HttpServletRequest request, HttpServletResponse View view = modelAndView.getView(); view.render(model, request, response); } - - public void addHandlerMapping(HandlerMapping handlerMapping) { - handlerMappingRegistry.addHandlerMapping(handlerMapping); - } } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerAdapter.java b/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerAdapter.java similarity index 74% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerAdapter.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/HandlerAdapter.java index 6acc610728..215453bc4c 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerAdapter.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerAdapter.java @@ -1,6 +1,5 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet; -import com.interface21.webmvc.servlet.ModelAndView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerExceptionResolver.java b/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerExceptionResolver.java new file mode 100644 index 0000000000..7561cb7aee --- /dev/null +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerExceptionResolver.java @@ -0,0 +1,13 @@ +package com.interface21.webmvc.servlet; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public interface HandlerExceptionResolver { + + ModelAndView resolveException( + HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws ServletException; + +} \ No newline at end of file diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerMapping.java similarity index 76% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerMapping.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/HandlerMapping.java index b7db458b93..82b83b9714 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerMapping.java @@ -1,4 +1,4 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet; import jakarta.servlet.http.HttpServletRequest; diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/handler/AnnotationHandlerMapping.java similarity index 97% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/handler/AnnotationHandlerMapping.java index 85b0a034d7..b87262a578 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/handler/AnnotationHandlerMapping.java @@ -1,8 +1,9 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.handler; import com.interface21.context.stereotype.Controller; import com.interface21.web.bind.annotation.RequestMapping; import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.webmvc.servlet.HandlerMapping; import jakarta.servlet.http.HttpServletRequest; import java.lang.reflect.Method; import java.util.HashMap; diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/ExecutionHandlerAdapter.java b/mvc/src/main/java/com/interface21/webmvc/servlet/handler/ExecutionHandlerAdapter.java similarity index 84% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/ExecutionHandlerAdapter.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/handler/ExecutionHandlerAdapter.java index 120a9ba614..bf2f41070b 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/ExecutionHandlerAdapter.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/handler/ExecutionHandlerAdapter.java @@ -1,5 +1,6 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.handler; +import com.interface21.webmvc.servlet.HandlerAdapter; import com.interface21.webmvc.servlet.ModelAndView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerAdapterRegistry.java b/mvc/src/main/java/com/interface21/webmvc/servlet/handler/HandlerAdapterRegistry.java similarity index 86% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerAdapterRegistry.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/handler/HandlerAdapterRegistry.java index 78dd2bab7e..9c76664a03 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerAdapterRegistry.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/handler/HandlerAdapterRegistry.java @@ -1,5 +1,6 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.handler; +import com.interface21.webmvc.servlet.HandlerAdapter; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -14,7 +15,6 @@ public HandlerAdapterRegistry() { public void initialize() { addHandlerAdapter(new ExecutionHandlerAdapter()); - addHandlerAdapter(new ControllerHandlerAdapter()); } public void addHandlerAdapter(HandlerAdapter handlerAdapter) { diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/com/interface21/webmvc/servlet/handler/HandlerExecution.java similarity index 97% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/handler/HandlerExecution.java index 8b81f8dc91..d6017feaa2 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/handler/HandlerExecution.java @@ -1,4 +1,4 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.handler; import com.interface21.webmvc.servlet.ModelAndView; import jakarta.servlet.http.HttpServletRequest; diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java b/mvc/src/main/java/com/interface21/webmvc/servlet/handler/HandlerKey.java similarity index 81% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/handler/HandlerKey.java index 55f8e593a1..d5007cc0ce 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/handler/HandlerKey.java @@ -1,7 +1,6 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.handler; import com.interface21.web.bind.annotation.RequestMethod; - import java.util.Objects; public class HandlerKey { @@ -24,8 +23,12 @@ public String toString() { @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/com/interface21/webmvc/servlet/mvc/tobe/HandlerMappingRegistry.java b/mvc/src/main/java/com/interface21/webmvc/servlet/handler/HandlerMappingRegistry.java similarity index 90% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerMappingRegistry.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/handler/HandlerMappingRegistry.java index ae88841320..7c80daef7b 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerMappingRegistry.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/handler/HandlerMappingRegistry.java @@ -1,5 +1,6 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.handler; +import com.interface21.webmvc.servlet.HandlerMapping; import jakarta.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/Controller.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/Controller.java deleted file mode 100644 index 20e714341a..0000000000 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/Controller.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.interface21.webmvc.servlet.mvc.asis; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -public interface Controller { - String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception; -} diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/ForwardController.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/ForwardController.java deleted file mode 100644 index ecceef15af..0000000000 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/ForwardController.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.interface21.webmvc.servlet.mvc.asis; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import java.util.Objects; - -public class ForwardController implements Controller { - - private final String path; - - public ForwardController(final String path) { - this.path = Objects.requireNonNull(path); - } - - @Override - public String execute(final HttpServletRequest request, final HttpServletResponse response) { - return path; - } -} diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/ControllerHandlerAdapter.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/ControllerHandlerAdapter.java deleted file mode 100644 index 2b6bee4f50..0000000000 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/ControllerHandlerAdapter.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.interface21.webmvc.servlet.mvc.tobe; - -import com.interface21.webmvc.servlet.ModelAndView; -import com.interface21.webmvc.servlet.mvc.asis.Controller; -import com.interface21.webmvc.servlet.view.JspView; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -public class ControllerHandlerAdapter implements HandlerAdapter { - - @Override - public boolean supports(Object handler) { - return handler instanceof Controller; - } - - @Override - public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - String viewName = ((Controller) handler).execute(request, response); - return new ModelAndView(new JspView(viewName)); - } -} diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/resolver/ArgumentHandlerExceptionResolver.java b/mvc/src/main/java/com/interface21/webmvc/servlet/resolver/ArgumentHandlerExceptionResolver.java new file mode 100644 index 0000000000..eb66e937d6 --- /dev/null +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/resolver/ArgumentHandlerExceptionResolver.java @@ -0,0 +1,37 @@ +package com.interface21.webmvc.servlet.resolver; + +import com.interface21.webmvc.servlet.HandlerExceptionResolver; +import com.interface21.webmvc.servlet.ModelAndView; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ArgumentHandlerExceptionResolver implements HandlerExceptionResolver { + + private static final Logger log = LoggerFactory.getLogger(ArgumentHandlerExceptionResolver.class); + + @Override + public ModelAndView resolveException( + HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws ServletException { + + Throwable cause = ex.getCause(); + + try { + if (cause instanceof IllegalArgumentException) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.setContentType("text/plain"); + response.getWriter().write(cause.getMessage()); + return null; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + log.error("Exception : {}", ex.getMessage(), ex); + throw new ServletException(ex.getMessage()); + } +} diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/view/JsonView.java b/mvc/src/main/java/com/interface21/webmvc/servlet/view/JsonView.java index ffe5ae6fe2..8bef4005cb 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/view/JsonView.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/view/JsonView.java @@ -1,14 +1,33 @@ package com.interface21.webmvc.servlet.view; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.interface21.web.http.MediaType; import com.interface21.webmvc.servlet.View; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; - +import java.io.IOException; import java.util.Map; public class JsonView implements View { + private final JsonMapper jsonMapper; + + public JsonView() { + this.jsonMapper = new JsonMapper(); + } + @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 IOException { + response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + + if (model.size() == 1) { + String singleValue = model.values().iterator().next().toString(); + response.getWriter().write(singleValue); + return; + } + + String json = jsonMapper.writeValueAsString(model); + response.getWriter().write(json); } } diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/handler/AnnotationHandlerMappingTest.java similarity index 99% rename from mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java rename to mvc/src/test/java/com/interface21/webmvc/servlet/handler/AnnotationHandlerMappingTest.java index bd48f4d7ac..504eae200c 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/handler/AnnotationHandlerMappingTest.java @@ -1,4 +1,4 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.handler; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecutionTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/handler/HandlerExecutionTest.java similarity index 94% rename from mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecutionTest.java rename to mvc/src/test/java/com/interface21/webmvc/servlet/handler/HandlerExecutionTest.java index 190cb5975c..5da313b019 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecutionTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/handler/HandlerExecutionTest.java @@ -1,4 +1,4 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.handler; import static org.assertj.core.api.Assertions.assertThatCode; diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/handler/HandlerKeyTest.java similarity index 91% rename from mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java rename to mvc/src/test/java/com/interface21/webmvc/servlet/handler/HandlerKeyTest.java index 8f226b747d..2291b1606e 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/handler/HandlerKeyTest.java @@ -1,4 +1,4 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.handler; import static org.assertj.core.api.Assertions.assertThat; diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/ControllerHandlerAdapterTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/ControllerHandlerAdapterTest.java deleted file mode 100644 index b55cd23885..0000000000 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/ControllerHandlerAdapterTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.interface21.webmvc.servlet.mvc.tobe; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.interface21.webmvc.servlet.ModelAndView; -import com.interface21.webmvc.servlet.mvc.asis.Controller; -import com.interface21.webmvc.servlet.view.JspView; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; - -class ControllerHandlerAdapterTest { - - @Test - @DisplayName("핸들러를 호출해 뷰 이름으로 JspView 만들어 반환한다.") - void handle() throws Exception { - // given - HandlerAdapter handlerAdapter = new ControllerHandlerAdapter(); - Controller loginViewController = (req, res) -> "/login.jsp"; - - // when - ModelAndView modelAndView = handlerAdapter.handle( - new MockHttpServletRequest(), - new MockHttpServletResponse(), - loginViewController); - - // then - assertThat(modelAndView.getModel()).isEmpty(); - assertThat(modelAndView.getView()).isInstanceOf(JspView.class); - } -} diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerMappingRegistryTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerMappingRegistryTest.java deleted file mode 100644 index 6068304c08..0000000000 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerMappingRegistryTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.interface21.webmvc.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 java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import samples.ManualController; - -class HandlerMappingRegistryTest { - - private static final String BASE_PACKAGE = "samples"; - - @Test - @DisplayName("서로 다른 핸들러 매핑이 존재해도 메뉴얼 핸들러를 찾을 수 있다.") - void getManualHandler() { - // given - HttpServletRequest request = mock(HttpServletRequest.class); - HandlerMappingRegistry registry = new HandlerMappingRegistry(BASE_PACKAGE); - registry.addHandlerMapping(new ManualHandlerMapping()); - registry.initialize(); - - // when - when(request.getRequestURI()).thenReturn("/manual"); - when(request.getMethod()).thenReturn("GET"); - Optional handler = registry.getHandler(request); - - // then - assertThat(handler).isPresent(); - assertThat(handler.get()).isInstanceOf(ManualController.class); - } - - @Test - @DisplayName("서로 다른 핸들러 매핑이 존재해도 어노테이션 핸들러를 찾을 수 있다.") - void getAnnotationHandler() { - // given - HttpServletRequest request = mock(HttpServletRequest.class); - HandlerMappingRegistry registry = new HandlerMappingRegistry(BASE_PACKAGE); - registry.addHandlerMapping(new ManualHandlerMapping()); - registry.initialize(); - - // when - when(request.getRequestURI()).thenReturn("/post-test"); - when(request.getMethod()).thenReturn("POST"); - Optional handler = registry.getHandler(request); - - // then - assertThat(handler).isPresent(); - assertThat(handler.get()).isInstanceOf(HandlerExecution.class); - } -} \ No newline at end of file diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/ManualHandlerMapping.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/ManualHandlerMapping.java deleted file mode 100644 index e0ed1752ae..0000000000 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/ManualHandlerMapping.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.interface21.webmvc.servlet.mvc.tobe; - -import com.interface21.webmvc.servlet.mvc.asis.Controller; -import jakarta.servlet.http.HttpServletRequest; -import java.util.HashMap; -import java.util.Map; -import samples.ManualController; - -public class ManualHandlerMapping implements HandlerMapping { - - private static final Map controllers = new HashMap<>(); - - public void initialize() { - controllers.put("/manual", new ManualController()); - } - - @Override - public Object getHandler(HttpServletRequest request) { - String requestURI = request.getRequestURI(); - return controllers.get(requestURI); - } -} diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/view/JsonViewTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/view/JsonViewTest.java new file mode 100644 index 0000000000..cc7e2f21a7 --- /dev/null +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/view/JsonViewTest.java @@ -0,0 +1,51 @@ +package com.interface21.webmvc.servlet.view; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class JsonViewTest { + + @Test + @DisplayName("Serializable 한 값을 JSON 으로 렌더링 한다.") + void renderWithPrimitive() throws IOException { + // given + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + Skill skill = new Skill(List.of("Korean", "English"), List.of("Javascript", "Java")); + Map model = Map.of("user", "seyang", "langs", skill); + JsonView jsonView = new JsonView(); + OutputStream outputStream = new ByteArrayOutputStream(); + PrintWriter writer = new PrintWriter(outputStream); + + // when + when(response.getWriter()).thenReturn(writer); + jsonView.render(model, request, response); + writer.flush(); + + // then + verify(response).setContentType("application/json;charset=UTF-8"); + assertThat(outputStream.toString()).satisfiesAnyOf( + s -> assertThat(s).isEqualTo( + "{\"langs\":{\"langs\":[\"Korean\",\"English\"],\"codes\":[\"Javascript\",\"Java\"]},\"user\":\"seyang\"}"), + s -> assertThat(s).isEqualTo( + "{\"user\":\"seyang\",\"langs\":{\"langs\":[\"Korean\",\"English\"],\"codes\":[\"Javascript\",\"Java\"]}}") + ); + } + + record Skill(List langs, List codes) { + } +} diff --git a/mvc/src/test/java/samples/ManualController.java b/mvc/src/test/java/samples/ManualController.java deleted file mode 100644 index 3990dd564c..0000000000 --- a/mvc/src/test/java/samples/ManualController.java +++ /dev/null @@ -1,14 +0,0 @@ -package samples; - - -import com.interface21.webmvc.servlet.mvc.asis.Controller; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -public class ManualController implements Controller { - - @Override - public String execute(HttpServletRequest req, HttpServletResponse res) { - return "/manual.jsp"; - } -}