diff --git a/app/src/main/java/com/techcourse/DispatcherServlet.java b/app/src/main/java/com/techcourse/DispatcherServlet.java deleted file mode 100644 index 277d8eed9a..0000000000 --- a/app/src/main/java/com/techcourse/DispatcherServlet.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.techcourse; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import webmvc.org.springframework.web.servlet.view.JspView; - -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() { - } - - @Override - public void init() { - manualHandlerMapping = new ManualHandlerMapping(); - manualHandlerMapping.initialize(); - } - - @Override - protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException { - 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); - 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); - requestDispatcher.forward(request, response); - } -} diff --git a/app/src/main/java/com/techcourse/TomcatStarter.java b/app/src/main/java/com/techcourse/TomcatStarter.java index 4f26f228e3..5da57846bf 100644 --- a/app/src/main/java/com/techcourse/TomcatStarter.java +++ b/app/src/main/java/com/techcourse/TomcatStarter.java @@ -1,5 +1,6 @@ package com.techcourse; +import com.techcourse.mvc.exception.UncheckedServletException; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.connector.Connector; diff --git a/app/src/main/java/com/techcourse/controller/RegisterController.java b/app/src/main/java/com/techcourse/controller/RegisterController.java index da62e5e8e9..bbea247cba 100644 --- a/app/src/main/java/com/techcourse/controller/RegisterController.java +++ b/app/src/main/java/com/techcourse/controller/RegisterController.java @@ -2,20 +2,25 @@ 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 { + @RequestMapping(value = "/register", method = RequestMethod.POST) + public ModelAndView 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); - return "redirect:/index.jsp"; + return new ModelAndView(new JspView("/index.jsp")); } } diff --git a/app/src/main/java/com/techcourse/controller/RegisterViewController.java b/app/src/main/java/com/techcourse/controller/RegisterViewController.java index 136962136d..f36475bc77 100644 --- a/app/src/main/java/com/techcourse/controller/RegisterViewController.java +++ b/app/src/main/java/com/techcourse/controller/RegisterViewController.java @@ -1,13 +1,18 @@ package com.techcourse.controller; +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 RegisterViewController implements Controller { +@Controller +public class RegisterViewController { - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { - return "/register.jsp"; + @RequestMapping(value = "/register", method = RequestMethod.GET) + public ModelAndView execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { + return new ModelAndView(new JspView("/register.jsp")); } } diff --git a/app/src/main/java/com/techcourse/mvc/DispatcherServlet.java b/app/src/main/java/com/techcourse/mvc/DispatcherServlet.java new file mode 100644 index 0000000000..aed127529a --- /dev/null +++ b/app/src/main/java/com/techcourse/mvc/DispatcherServlet.java @@ -0,0 +1,46 @@ +package com.techcourse.mvc; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.mvc.HandlerAdapter; +import webmvc.org.springframework.web.servlet.mvc.HandlerMapping; + +public class DispatcherServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class); + + private HandlerMapping handlerMapping; + private HandlerAdapter handlerAdapter; + + public DispatcherServlet(final HandlerMapping handlerMapping, final HandlerAdapter handlerAdapter) { + this.handlerMapping = handlerMapping; + this.handlerAdapter = handlerAdapter; + } + + @Override + public void init() { + handlerMapping.initialize(); + } + + @Override + protected void service(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException { + final String requestURI = request.getRequestURI(); + log.debug("Method : {}, Request URI : {}", request.getMethod(), requestURI); + + try { + final Object handler = handlerMapping.getHandler(request); + final ModelAndView modelAndView = handlerAdapter.handle(request, response, handler); + modelAndView.render(request, response); + } catch (Throwable e) { + log.error("Exception : {}", e.getMessage(), e); + throw new ServletException(e.getMessage()); + } + } +} diff --git a/app/src/main/java/com/techcourse/DispatcherServletInitializer.java b/app/src/main/java/com/techcourse/mvc/DispatcherServletInitializer.java similarity index 51% rename from app/src/main/java/com/techcourse/DispatcherServletInitializer.java rename to app/src/main/java/com/techcourse/mvc/DispatcherServletInitializer.java index 6e814cdd25..394f77784f 100644 --- a/app/src/main/java/com/techcourse/DispatcherServletInitializer.java +++ b/app/src/main/java/com/techcourse/mvc/DispatcherServletInitializer.java @@ -1,9 +1,13 @@ -package com.techcourse; +package com.techcourse.mvc; import jakarta.servlet.ServletContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import web.org.springframework.web.WebApplicationInitializer; +import webmvc.org.springframework.web.servlet.mvc.CompositeHandlerAdapter; +import webmvc.org.springframework.web.servlet.mvc.CompositeHandlerMapping; +import webmvc.org.springframework.web.servlet.mvc.tobe.AnnotationHandlerAdapter; +import webmvc.org.springframework.web.servlet.mvc.tobe.AnnotationHandlerMapping; /** * Base class for {@link WebApplicationInitializer} @@ -17,7 +21,7 @@ public class DispatcherServletInitializer implements WebApplicationInitializer { @Override public void onStartup(final ServletContext servletContext) { - final var dispatcherServlet = new DispatcherServlet(); + final var dispatcherServlet = new DispatcherServlet(initHandlerMapping(), initHandlerAdapter()); final var registration = servletContext.addServlet(DEFAULT_SERVLET_NAME, dispatcherServlet); if (registration == null) { @@ -30,4 +34,16 @@ public void onStartup(final ServletContext servletContext) { log.info("Start AppWebApplication Initializer"); } + + private CompositeHandlerAdapter initHandlerAdapter() { + final var annotationHandlerAdapter = new AnnotationHandlerAdapter(); + final var manualHandlerAdapter = new ManualHandlerAdapter(); + return new CompositeHandlerAdapter(manualHandlerAdapter, annotationHandlerAdapter); + } + + private CompositeHandlerMapping initHandlerMapping() { + final var annotationHandlerMapping = new AnnotationHandlerMapping(); + final var manualHandlerMappingAdapter = new ManualHandlerMappingAdapter(); + return new CompositeHandlerMapping(manualHandlerMappingAdapter, annotationHandlerMapping); + } } diff --git a/app/src/main/java/com/techcourse/mvc/ManualHandlerAdapter.java b/app/src/main/java/com/techcourse/mvc/ManualHandlerAdapter.java new file mode 100644 index 0000000000..402e9f4385 --- /dev/null +++ b/app/src/main/java/com/techcourse/mvc/ManualHandlerAdapter.java @@ -0,0 +1,30 @@ +package com.techcourse.mvc; + +import com.techcourse.mvc.exception.CanNotInvokeMethodException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.mvc.HandlerAdapter; +import webmvc.org.springframework.web.servlet.mvc.asis.Controller; +import webmvc.org.springframework.web.servlet.view.JspView; + +public class ManualHandlerAdapter implements HandlerAdapter { + + @Override + public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response, + final Object handler) { + try { + final var controller = (Controller) handler; + final var viewName = controller.execute(request, response); + return new ModelAndView(new JspView(viewName)); + } catch (Exception e) { + e.printStackTrace(); + throw new CanNotInvokeMethodException(); + } + } + + @Override + public boolean supports(Object handler) { + return handler instanceof Controller; + } +} diff --git a/app/src/main/java/com/techcourse/ManualHandlerMapping.java b/app/src/main/java/com/techcourse/mvc/ManualHandlerMapping.java similarity index 84% rename from app/src/main/java/com/techcourse/ManualHandlerMapping.java rename to app/src/main/java/com/techcourse/mvc/ManualHandlerMapping.java index a54863caf8..a89006cf5c 100644 --- a/app/src/main/java/com/techcourse/ManualHandlerMapping.java +++ b/app/src/main/java/com/techcourse/mvc/ManualHandlerMapping.java @@ -1,6 +1,8 @@ -package com.techcourse; +package com.techcourse.mvc; -import com.techcourse.controller.*; +import com.techcourse.controller.LoginController; +import com.techcourse.controller.LoginViewController; +import com.techcourse.controller.LogoutController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import webmvc.org.springframework.web.servlet.mvc.asis.Controller; @@ -20,8 +22,6 @@ public void initialize() { 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() diff --git a/app/src/main/java/com/techcourse/mvc/ManualHandlerMappingAdapter.java b/app/src/main/java/com/techcourse/mvc/ManualHandlerMappingAdapter.java new file mode 100644 index 0000000000..26f03f7af2 --- /dev/null +++ b/app/src/main/java/com/techcourse/mvc/ManualHandlerMappingAdapter.java @@ -0,0 +1,23 @@ +package com.techcourse.mvc; + +import jakarta.servlet.http.HttpServletRequest; +import webmvc.org.springframework.web.servlet.mvc.HandlerMapping; + +public class ManualHandlerMappingAdapter implements HandlerMapping { + + private final ManualHandlerMapping manualHandlerMapping; + + public ManualHandlerMappingAdapter() { + manualHandlerMapping = new ManualHandlerMapping(); + } + + @Override + public void initialize() { + manualHandlerMapping.initialize(); + } + + @Override + public Object getHandler(final HttpServletRequest httpServletRequest) { + return manualHandlerMapping.getHandler(httpServletRequest.getRequestURI()); + } +} diff --git a/app/src/main/java/com/techcourse/mvc/exception/CanNotInvokeMethodException.java b/app/src/main/java/com/techcourse/mvc/exception/CanNotInvokeMethodException.java new file mode 100644 index 0000000000..d76b67b41e --- /dev/null +++ b/app/src/main/java/com/techcourse/mvc/exception/CanNotInvokeMethodException.java @@ -0,0 +1,4 @@ +package com.techcourse.mvc.exception; + +public class CanNotInvokeMethodException extends RuntimeException { +} diff --git a/app/src/main/java/com/techcourse/UncheckedServletException.java b/app/src/main/java/com/techcourse/mvc/exception/UncheckedServletException.java similarity index 79% rename from app/src/main/java/com/techcourse/UncheckedServletException.java rename to app/src/main/java/com/techcourse/mvc/exception/UncheckedServletException.java index 26acea7605..a226a05044 100644 --- a/app/src/main/java/com/techcourse/UncheckedServletException.java +++ b/app/src/main/java/com/techcourse/mvc/exception/UncheckedServletException.java @@ -1,4 +1,4 @@ -package com.techcourse; +package com.techcourse.mvc.exception; public class UncheckedServletException extends RuntimeException { diff --git a/app/src/test/java/com/techcourse/mvc/DispatcherServletTest.java b/app/src/test/java/com/techcourse/mvc/DispatcherServletTest.java new file mode 100644 index 0000000000..18bf9659d1 --- /dev/null +++ b/app/src/test/java/com/techcourse/mvc/DispatcherServletTest.java @@ -0,0 +1,64 @@ +package com.techcourse.mvc; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; +import webmvc.org.springframework.web.servlet.mvc.tobe.AnnotationHandlerAdapter; +import webmvc.org.springframework.web.servlet.mvc.tobe.AnnotationHandlerMapping; + +import static org.mockito.Mockito.*; + +class DispatcherServletTest { + + @Test + void service_RegisterViewController() throws Exception { + // given + final var handlerMapping = new AnnotationHandlerMapping("com.techcourse.controller"); + handlerMapping.initialize(); + final var handlerAdapter = new AnnotationHandlerAdapter(); + final var dispatcherServlet = new DispatcherServlet(handlerMapping, handlerAdapter); + + final var request = mock(HttpServletRequest.class); + final var response = mock(HttpServletResponse.class); + final var requestDispatcher = mock(RequestDispatcher.class); + + when(request.getRequestURI()).thenReturn("/register"); + when(request.getMethod()).thenReturn("GET"); + when(request.getRequestDispatcher("/register.jsp")).thenReturn(requestDispatcher); + doNothing().when(requestDispatcher).forward(any(), any()); + + // when + dispatcherServlet.service(request, response); + + // then + verify(request, times(1)).getRequestDispatcher("/register.jsp"); + verify(requestDispatcher, times(1)).forward(request, response); + } + + @Test + void service_RegisterController() throws Exception { + // given + final var handlerMapping = new AnnotationHandlerMapping("com.techcourse.controller"); + handlerMapping.initialize(); + final var handlerAdapter = new AnnotationHandlerAdapter(); + final var dispatcherServlet = new DispatcherServlet(handlerMapping, handlerAdapter); + + final var request = mock(HttpServletRequest.class); + final var response = mock(HttpServletResponse.class); + final var requestDispatcher = mock(RequestDispatcher.class); + + when(request.getRequestURI()).thenReturn("/register"); + when(request.getMethod()).thenReturn("POST"); + when(request.getParameter(any())).thenReturn("anyParam"); + when(request.getRequestDispatcher("/index.jsp")).thenReturn(requestDispatcher); + doNothing().when(requestDispatcher).forward(any(), any()); + + // when + dispatcherServlet.service(request, response); + + // then + verify(request, times(1)).getRequestDispatcher("/index.jsp"); + verify(requestDispatcher, times(1)).forward(request, response); + } +} diff --git a/app/src/test/java/com/techcourse/mvc/ManualHandlerAdapterTest.java b/app/src/test/java/com/techcourse/mvc/ManualHandlerAdapterTest.java new file mode 100644 index 0000000000..c0506e2c7c --- /dev/null +++ b/app/src/test/java/com/techcourse/mvc/ManualHandlerAdapterTest.java @@ -0,0 +1,51 @@ +package com.techcourse.mvc; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import samples.TestController; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.view.JspView; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; + +class ManualHandlerAdapterTest { + + private ManualHandlerAdapter manualHandlerAdapter; + + @BeforeEach + void setUp() { + manualHandlerAdapter = new ManualHandlerAdapter(); + } + + @Test + void handle() { + // given + final var request = mock(HttpServletRequest.class); + final var response = mock(HttpServletResponse.class); + + final var handler = new TestController(); + + // when + final ModelAndView result = manualHandlerAdapter.handle(request, response, handler); + + // then + assertThat(result.getView()) + .usingRecursiveComparison() + .isEqualTo(new JspView("/test.jsp")); + } + + @Test + void supports() { + // given + final var handler = new TestController(); + + // when + final boolean supports = manualHandlerAdapter.supports(handler); + + // then + assertThat(supports).isTrue(); + } +} diff --git a/app/src/test/java/samples/TestController.java b/app/src/test/java/samples/TestController.java new file mode 100644 index 0000000000..fb2abff772 --- /dev/null +++ b/app/src/test/java/samples/TestController.java @@ -0,0 +1,13 @@ +package samples; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.mvc.asis.Controller; + +public class TestController implements Controller { + + @Override + public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { + return "/test.jsp"; + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java index ff8e24553f..3b848c6857 100644 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java @@ -1,5 +1,8 @@ package webmvc.org.springframework.web.servlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -14,6 +17,10 @@ public ModelAndView(final View view) { this.model = new HashMap<>(); } + public void render(HttpServletRequest request, HttpServletResponse response) throws Exception { + view.render(model, request, response); + } + public ModelAndView addObject(final String attributeName, final Object attributeValue) { model.put(attributeName, attributeValue); return this; diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/CompositeHandlerAdapter.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/CompositeHandlerAdapter.java new file mode 100644 index 0000000000..b61266eaa2 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/CompositeHandlerAdapter.java @@ -0,0 +1,34 @@ +package webmvc.org.springframework.web.servlet.mvc; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.mvc.exception.HandlerAdapterNotFoundException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class CompositeHandlerAdapter implements HandlerAdapter { + + private final List handlerAdapters = new ArrayList<>(); + + public CompositeHandlerAdapter(HandlerAdapter... args) { + handlerAdapters.addAll(Arrays.asList(args)); + } + + @Override + public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) { + return handlerAdapters.stream() + .filter(adapter -> adapter.supports(handler)) + .map(adapter -> adapter.handle(request, response, handler)) + .findAny() + .orElseThrow(HandlerAdapterNotFoundException::new); + } + + @Override + public boolean supports(Object handler) { + return handlerAdapters.stream() + .anyMatch(adapter -> adapter.supports(handler)); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/CompositeHandlerMapping.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/CompositeHandlerMapping.java new file mode 100644 index 0000000000..8af1c2e772 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/CompositeHandlerMapping.java @@ -0,0 +1,32 @@ +package webmvc.org.springframework.web.servlet.mvc; + +import jakarta.servlet.http.HttpServletRequest; +import webmvc.org.springframework.web.servlet.mvc.exception.HandlerMappingNotFoundException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class CompositeHandlerMapping implements HandlerMapping { + + private final List handlerMappings = new ArrayList<>(); + + public CompositeHandlerMapping(HandlerMapping... args) { + handlerMappings.addAll(Arrays.asList(args)); + } + + @Override + public void initialize() { + handlerMappings.forEach(HandlerMapping::initialize); + } + + @Override + public Object getHandler(final HttpServletRequest httpServletRequest) { + return handlerMappings.stream() + .map(handlerMapping -> handlerMapping.getHandler(httpServletRequest)) + .filter(Objects::nonNull) + .findAny() + .orElseThrow(HandlerMappingNotFoundException::new); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdapter.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdapter.java new file mode 100644 index 0000000000..489fbbb5ec --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdapter.java @@ -0,0 +1,12 @@ +package webmvc.org.springframework.web.servlet.mvc; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.ModelAndView; + +public interface HandlerAdapter { + + ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler); + + boolean supports(Object handler); +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerMapping.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerMapping.java new file mode 100644 index 0000000000..c6c902efc9 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerMapping.java @@ -0,0 +1,10 @@ +package webmvc.org.springframework.web.servlet.mvc; + +import jakarta.servlet.http.HttpServletRequest; + +public interface HandlerMapping { + + void initialize(); + + Object getHandler(final HttpServletRequest httpServletRequest); +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/CanNotHandleException.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/CanNotHandleException.java new file mode 100644 index 0000000000..0c4fea1912 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/CanNotHandleException.java @@ -0,0 +1,4 @@ +package webmvc.org.springframework.web.servlet.mvc.exception; + +public class CanNotHandleException extends RuntimeException { +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/HandlerAdapterNotFoundException.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/HandlerAdapterNotFoundException.java new file mode 100644 index 0000000000..40551e47c3 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/HandlerAdapterNotFoundException.java @@ -0,0 +1,4 @@ +package webmvc.org.springframework.web.servlet.mvc.exception; + +public class HandlerAdapterNotFoundException extends RuntimeException { +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/HandlerMappingNotFoundException.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/HandlerMappingNotFoundException.java new file mode 100644 index 0000000000..363200a6d5 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/HandlerMappingNotFoundException.java @@ -0,0 +1,4 @@ +package webmvc.org.springframework.web.servlet.mvc.exception; + +public class HandlerMappingNotFoundException extends RuntimeException { +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapter.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapter.java new file mode 100644 index 0000000000..a54bd6ed6b --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapter.java @@ -0,0 +1,24 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.mvc.HandlerAdapter; +import webmvc.org.springframework.web.servlet.mvc.exception.CanNotHandleException; + +public class AnnotationHandlerAdapter implements HandlerAdapter { + + @Override + public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) { + try { + return ((HandlerExecution) handler).handle(request, response); + } catch (Exception e) { + throw new CanNotHandleException(); + } + } + + @Override + public boolean supports(Object handler) { + return handler instanceof HandlerExecution; + } +} 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/AnnotationHandlerMapping.java index f99505a339..3a18ce900b 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/AnnotationHandlerMapping.java @@ -8,16 +8,19 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import org.reflections.Reflections; 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.HandlerMapping; import webmvc.org.springframework.web.servlet.mvc.exception.CanNotInstanceHandlerException; -public class AnnotationHandlerMapping { +public class AnnotationHandlerMapping implements HandlerMapping { private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class); private static final Set> supportParameters = Set.of(HttpServletRequest.class, @@ -31,6 +34,7 @@ public AnnotationHandlerMapping(final Object... basePackage) { this.handlerExecutions = new HashMap<>(); } + @Override public void initialize() { final var reflections = new Reflections(basePackage); final var classes = reflections.getTypesAnnotatedWith(Controller.class); @@ -38,14 +42,21 @@ public void initialize() { final var handler = getHandlerInstance(clazz); final var methods = clazz.getMethods(); - Arrays.stream(methods) - .filter(this::supportParameters) - .filter(method -> method.isAnnotationPresent(RequestMapping.class)) - .forEach(method -> putHandlerExecutions(handler, method)); + final var executionMap = createHandlerExecutionMap(handler, methods); + handlerExecutions.putAll(executionMap); } log.info("Initialized AnnotationHandlerMapping!"); } + private Map createHandlerExecutionMap(final Object handler, final Method[] methods) { + return Arrays.stream(methods) + .filter(this::supportParameters) + .filter(method -> method.isAnnotationPresent(RequestMapping.class)) + .map(method -> toHandlerExecutions(handler, method)) + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + } + private boolean supportParameters(final Method method) { final var parameterTypes = Arrays.stream(method.getParameterTypes()) .collect(Collectors.toList()); @@ -53,31 +64,33 @@ private boolean supportParameters(final Method method) { return supportParameters.containsAll(parameterTypes) && parameterTypes.size() == 2; } - private void putHandlerExecutions(final Object handler, final Method method) { + private Map toHandlerExecutions( + final Object handler, final Method method) { final var annotation = method.getDeclaredAnnotation(RequestMapping.class); final var handlerExecution = new HandlerExecution(handler, method); - Arrays.stream(annotation.method()) + return Arrays.stream(annotation.method()) .map(requestMethod -> new HandlerKey(annotation.value(), requestMethod)) - .forEach(handlerKey -> handlerExecutions.put(handlerKey, handlerExecution)); + .collect(Collectors.toMap(Function.identity(), handlerKey -> handlerExecution)); } private static Object getHandlerInstance(final Class clazz) { try { return clazz.getConstructor().newInstance(); } catch (NoSuchMethodException | - IllegalAccessException | - InstantiationException | + IllegalAccessException | + InstantiationException | InvocationTargetException e ) { throw new CanNotInstanceHandlerException(); } } - public Object getHandler(final HttpServletRequest request) { - String requestURI = request.getRequestURI(); - RequestMethod requestMethod = RequestMethod.valueOf(request.getMethod()); - HandlerKey handlerKey = new HandlerKey(requestURI, requestMethod); + @Override + public HandlerExecution getHandler(final HttpServletRequest request) { + final var requestURI = request.getRequestURI(); + final var requestMethod = RequestMethod.valueOf(request.getMethod()); + final var handlerKey = new HandlerKey(requestURI, requestMethod); return handlerExecutions.get(handlerKey); } 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 index c33b246517..4bfe9d9f61 100644 --- 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 @@ -19,4 +19,12 @@ public ModelAndView handle(final HttpServletRequest request, final HttpServletRe throws Exception { return (ModelAndView) method.invoke(handler, request, response); } + + public Object getHandler() { + return handler; + } + + public Method getMethod() { + return method; + } } 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..18f654d28d 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 @@ -14,18 +14,25 @@ public class JspView implements View { 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 - model.keySet().forEach(key -> { log.debug("attribute name : {}, value : {}", key, model.get(key)); request.setAttribute(key, model.get(key)); }); - // todo + if (viewName.startsWith(JspView.REDIRECT_PREFIX)) { + response.sendRedirect(viewName.substring(JspView.REDIRECT_PREFIX.length())); + return; + } + + final var requestDispatcher = request.getRequestDispatcher(viewName); + requestDispatcher.forward(request, response); } } diff --git a/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapterTest.java b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapterTest.java new file mode 100644 index 0000000000..d856695b90 --- /dev/null +++ b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapterTest.java @@ -0,0 +1,59 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import samples.TestController; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.view.JspView; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AnnotationHandlerAdapterTest { + + private AnnotationHandlerAdapter handlerAdapter; + + @BeforeEach + void setUp() { + handlerAdapter = new AnnotationHandlerAdapter(); + } + + @Test + void handle() throws Exception { + // given + final var request = mock(HttpServletRequest.class); + final var response = mock(HttpServletResponse.class); + + final var testController = new TestController(); + final var method = testController.getClass().getMethod("findUserId", HttpServletRequest.class, HttpServletResponse.class); + final var handlerExecution = new HandlerExecution(testController, method); + + when(request.getAttribute("id")).thenReturn("maco"); + + // when + final ModelAndView result = handlerAdapter.handle(request, response, handlerExecution); + + // then + assertThat(result.getView()) + .usingRecursiveComparison() + .isEqualTo(new JspView("")); + assertThat(result.getObject("id")).isEqualTo("maco"); + } + + @Test + void supports() throws Exception { + // given + final var testController = new TestController(); + final var method = testController.getClass().getMethod("findUserId", HttpServletRequest.class, HttpServletResponse.class); + final var handlerExecution = new HandlerExecution(testController, method); + + // when + final boolean result = handlerAdapter.supports(handlerExecution); + + // then + assertThat(result).isTrue(); + } +}