diff --git a/.gitignore b/.gitignore index 3fefc442..29d7114a 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ bin/ ### Mac OS ### .DS_Store\ntomcat.8080/ + +## tomcat +**/tomcat.8080 diff --git a/build.gradle.kts b/build.gradle.kts index fab62014..d870a9ed 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,9 @@ dependencies { implementation("org.apache.tomcat.embed:tomcat-embed-core:8.5.42") implementation("org.apache.tomcat.embed:tomcat-embed-jasper:8.5.42") + // 리플렉션 의존성 주입 + implementation("org.reflections:reflections:0.10.2") + implementation(kotlin("stdlib-jdk8")) } diff --git a/src/main/java/com/diy/app/Lecture.java b/src/main/java/com/diy/app/Lecture.java new file mode 100644 index 00000000..2fc4f9d5 --- /dev/null +++ b/src/main/java/com/diy/app/Lecture.java @@ -0,0 +1,29 @@ +package com.diy.app; + +import java.math.BigDecimal; + +public class Lecture { + + private Long id; + private String name; + private BigDecimal price; + + public Lecture() { + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public BigDecimal getPrice() { + return price; + } + + public void setId(Long id) { + this.id = id; + } +} diff --git a/src/main/java/com/diy/app/LectureApplication.java b/src/main/java/com/diy/app/LectureApplication.java new file mode 100644 index 00000000..57e1381f --- /dev/null +++ b/src/main/java/com/diy/app/LectureApplication.java @@ -0,0 +1,20 @@ +package com.diy.app; + +import com.diy.framework.context.ApplicationContext; +import com.diy.framework.web.mvc.Controller; +import com.diy.framework.web.server.TomcatWebServer; +import com.diy.framework.web.servlet.DispatcherServlet; + +import java.util.Map; + +public class LectureApplication { + public static void main(String[] args) { + final ApplicationContext ac = new ApplicationContext(LectureApplication.class.getPackageName()); + ac.initialize(); + + final Map controllerMapping = ac.getControllerMapping(); + final DispatcherServlet servlet = new DispatcherServlet(controllerMapping); + final TomcatWebServer tomcatWebServer = new TomcatWebServer(servlet); + tomcatWebServer.start(); + } +} diff --git a/src/main/java/com/diy/app/LectureController.java b/src/main/java/com/diy/app/LectureController.java new file mode 100644 index 00000000..4f129b51 --- /dev/null +++ b/src/main/java/com/diy/app/LectureController.java @@ -0,0 +1,56 @@ +package com.diy.app; + +import com.diy.framework.context.annotation.Component; +import com.diy.framework.context.annotation.RequestMapping; +import com.diy.framework.web.mvc.Controller; +import com.diy.framework.web.mvc.view.ModelAndView; +import com.fasterxml.jackson.databind.ObjectMapper; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +@Component +@RequestMapping("/lectures") +public class LectureController implements Controller { + + private final Map lectureRepository = new HashMap<>(); + + @Override + public ModelAndView handleRequest(final HttpServletRequest request, final HttpServletResponse response) throws Exception { + if ("POST".equals(request.getMethod())) { + return doPost(request, response); + } else if ("GET".equals(request.getMethod())) { + return doGet(request, response); + } + + throw new RuntimeException("404 Not Found"); + } + + private ModelAndView doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { + final byte[] bodyBytes = req.getInputStream().readAllBytes(); + final String body = new String(bodyBytes, StandardCharsets.UTF_8); + + final Lecture lecture = new ObjectMapper().readValue(body, Lecture.class); + + final long id = lectureRepository.size(); + lectureRepository.put(id, lecture); + lecture.setId(id); + + return new ModelAndView("redirect:/lectures"); + } + + private ModelAndView doGet(final HttpServletRequest req, final HttpServletResponse resp) throws Exception { + final Collection lectures = lectureRepository.values(); + final Map model = new HashMap<>(); + final Object lectureModels = lectures.stream().map(lecture -> Map.of("id", lecture.getId(), "name", lecture.getName(), "price", lecture.getPrice())).toList(); + model.put("lectures", lectureModels); + + return new ModelAndView("lecture-list", model); + } +} diff --git a/src/main/java/com/diy/app/Main.java b/src/main/java/com/diy/app/Main.java deleted file mode 100644 index c81e36ae..00000000 --- a/src/main/java/com/diy/app/Main.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.diy.app; - -public class Main { - public static void main(String[] args) { - } -} diff --git a/src/main/java/com/diy/framework/beans/factory/BeanScanner.java b/src/main/java/com/diy/framework/beans/factory/BeanScanner.java new file mode 100644 index 00000000..fd2c6b2b --- /dev/null +++ b/src/main/java/com/diy/framework/beans/factory/BeanScanner.java @@ -0,0 +1,23 @@ +package com.diy.framework.beans.factory; + +import org.reflections.Reflections; + +import java.lang.annotation.Annotation; +import java.util.Set; +import java.util.stream.Collectors; + +public class BeanScanner { + + private final Reflections reflections; + + public BeanScanner(final String... basePackages) { + reflections = new Reflections(basePackages); + } + + public Set> scanClassesTypeAnnotatedWith(final Class annotation) { + return reflections.getTypesAnnotatedWith(annotation) + .stream() + .filter(type -> (!type.isAnnotation() && !type.isInterface())) + .collect(Collectors.toSet()); + } +} \ No newline at end of file diff --git a/src/main/java/com/diy/framework/context/ApplicationContext.java b/src/main/java/com/diy/framework/context/ApplicationContext.java new file mode 100644 index 00000000..08c67a02 --- /dev/null +++ b/src/main/java/com/diy/framework/context/ApplicationContext.java @@ -0,0 +1,155 @@ +package com.diy.framework.context; + +import com.diy.framework.beans.factory.BeanScanner; +import com.diy.framework.context.annotation.Autowired; +import com.diy.framework.context.annotation.Bean; +import com.diy.framework.context.annotation.Component; +import com.diy.framework.context.annotation.RequestMapping; +import com.diy.framework.web.mvc.Controller; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.*; + +public class ApplicationContext { + + private final String basePackage; + private final Set> beanClasses = new HashSet<>(); + private final Map beans = new HashMap<>(); + + public ApplicationContext(final String basePackage) { + this.basePackage = basePackage; + } + + public void initialize() { + final BeanScanner beanScanner = new BeanScanner(basePackage); + beanClasses.addAll(beanScanner.scanClassesTypeAnnotatedWith(Component.class)); + + beanClasses.forEach(clazz -> { + if (isBeanInitialized(clazz)) { + return; + } + + final Object bean = createInstance(clazz); + saveBean(generateBeanName(clazz), bean); + }); + + registerBeanMethods(); + } + + private void registerBeanMethods() { + // 이미 등록된 @Component 빈들을 순회 + new ArrayList<>(beans.values()).forEach(componentBean -> { + final Method[] methods = componentBean.getClass().getDeclaredMethods(); + Arrays.stream(methods) + .filter(method -> method.isAnnotationPresent(Bean.class)) + .forEach(method -> registerBeanFromMethod(componentBean, method)); + }); + } + + private void registerBeanFromMethod(final Object componentBean, final Method method) { + try { + final Bean beanAnnotation = method.getAnnotation(Bean.class); + + // 이름: @Bean("name")이면 name, 없으면 메서드명 + final String beanName = beanAnnotation.value().isEmpty() + ? method.getName() + : beanAnnotation.value(); + + method.setAccessible(true); + final Object bean = method.invoke(componentBean); + method.setAccessible(false); + + saveBean(beanName, bean); + } catch (Exception e) { + throw new RuntimeException("@Bean 메서드 실행 실패", e); + } + } + + private Object createInstance(final Class clazz) { + final Constructor constructor = findConstructor(clazz); + + try { + constructor.setAccessible(true); + final Object[] parameters = getConstructorParameters(constructor); + + return constructor.newInstance(parameters); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + constructor.setAccessible(false); + } + } + + private Constructor findConstructor(final Class clazz) { + final Constructor[] constructors = clazz.getDeclaredConstructors(); + + if (constructors.length == 1) { + return constructors[0]; + } + + return findAutowiredConstructor(constructors); + } + + private Constructor findAutowiredConstructor(final Constructor[] constructors) { + final Constructor[] autowiredConstructors = Arrays.stream(constructors) + .filter(constructor -> constructor.isAnnotationPresent(Autowired.class)) + .toArray(Constructor[]::new); + + if (autowiredConstructors.length != 1) { + throw new RuntimeException("Autowired constructor not found"); + } + + return autowiredConstructors[0]; + } + + private Object[] getConstructorParameters(final Constructor constructor) { + final List> parameterTypes = Arrays.stream(constructor.getParameterTypes()).toList(); + + if (!beanClasses.containsAll(parameterTypes)) { + throw new RuntimeException("parameter is not bean"); + } + + return parameterTypes.stream().map(parameterType -> { + if (isBeanInitialized(parameterType)) { + return beans.values().stream() + .filter(bean -> bean.getClass().equals(parameterType)) + .findFirst().get(); + } + + final Object bean = createInstance(parameterType); + saveBean(generateBeanName(parameterType), bean); + + return bean; + }).toArray(); + } + + public Map getControllerMapping() { + final Map mapping = new HashMap<>(); + + beans.values().stream() + .filter(bean -> bean instanceof Controller) + .forEach(bean -> { + final RequestMapping annotation = + bean.getClass().getAnnotation(RequestMapping.class); + if (annotation != null) { + mapping.put(annotation.value(), (Controller) bean); + } + }); + + return mapping; + } + + private boolean isBeanInitialized(final Class clazz) { + return beans.values().stream().anyMatch(bean -> bean.getClass().equals(clazz)); + } + + private void saveBean(final String beanName, final Object bean) { + beans.put(beanName, bean); + } + + private String generateBeanName(final Class clazz) { + final String simpleName = clazz.getSimpleName(); + return Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1); + } +} diff --git a/src/main/java/com/diy/framework/context/annotation/Autowired.java b/src/main/java/com/diy/framework/context/annotation/Autowired.java new file mode 100644 index 00000000..e38e425f --- /dev/null +++ b/src/main/java/com/diy/framework/context/annotation/Autowired.java @@ -0,0 +1,11 @@ +package com.diy.framework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Autowired { +} diff --git a/src/main/java/com/diy/framework/context/annotation/Bean.java b/src/main/java/com/diy/framework/context/annotation/Bean.java new file mode 100644 index 00000000..658e12eb --- /dev/null +++ b/src/main/java/com/diy/framework/context/annotation/Bean.java @@ -0,0 +1,12 @@ +package com.diy.framework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Bean { + String value() default ""; +} diff --git a/src/main/java/com/diy/framework/context/annotation/Component.java b/src/main/java/com/diy/framework/context/annotation/Component.java new file mode 100644 index 00000000..22a0c9a3 --- /dev/null +++ b/src/main/java/com/diy/framework/context/annotation/Component.java @@ -0,0 +1,11 @@ +package com.diy.framework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Component { +} diff --git a/src/main/java/com/diy/framework/context/annotation/RequestMapping.java b/src/main/java/com/diy/framework/context/annotation/RequestMapping.java new file mode 100644 index 00000000..fe455f68 --- /dev/null +++ b/src/main/java/com/diy/framework/context/annotation/RequestMapping.java @@ -0,0 +1,12 @@ +package com.diy.framework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface RequestMapping { + String value(); +} diff --git a/src/main/java/com/diy/framework/web/mvc/Controller.java b/src/main/java/com/diy/framework/web/mvc/Controller.java new file mode 100644 index 00000000..cf802167 --- /dev/null +++ b/src/main/java/com/diy/framework/web/mvc/Controller.java @@ -0,0 +1,11 @@ +package com.diy.framework.web.mvc; + +import com.diy.framework.web.mvc.view.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@FunctionalInterface +public interface Controller { + ModelAndView handleRequest(final HttpServletRequest request, final HttpServletResponse response) throws Exception; +} diff --git a/src/main/java/com/diy/framework/web/mvc/view/HtmlView.java b/src/main/java/com/diy/framework/web/mvc/view/HtmlView.java new file mode 100644 index 00000000..916bab9f --- /dev/null +++ b/src/main/java/com/diy/framework/web/mvc/view/HtmlView.java @@ -0,0 +1,79 @@ +package com.diy.framework.web.mvc.view; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class HtmlView implements View { + + final Pattern pattern = Pattern.compile(""); + + private final String viewName; + + public HtmlView(final String viewName) { + this.viewName = viewName; + } + + @Override + public void render(final Map model, final HttpServletRequest req, final HttpServletResponse res) throws IOException { + final String viewFile = readViewFile(req, model); + + res.setContentType("text/html;charset=utf-8"); + final PrintWriter writer = res.getWriter(); + writer.print(viewFile); + } + + private String readViewFile(final HttpServletRequest req, final Map model) { + final StringBuilder content = new StringBuilder(); + + final String viewPath = getViewPath(req); + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(viewPath), StandardCharsets.UTF_8))) { + String line; + + + while ((line = reader.readLine()) != null) { + final Matcher matcher = pattern.matcher(line); + + if (matcher.find()) { + final String modelKey = matcher.group(1); + final List> lectures = (List>) model.get(modelKey); + + lectures.forEach(lecture -> { + lecture.forEach((key, value) -> + content.append("
  • ") + .append(key) + .append(": ") + .append(value) + .append("
  • ") + .append("\n")); + + content.append("
    ").append("\n"); + }); + + continue; + } + + content.append(line).append("\n"); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + return content.toString(); + } + + private String getViewPath(final HttpServletRequest req) { + final ServletContext sc = req.getServletContext(); + return sc.getRealPath(viewName); + } +} diff --git a/src/main/java/com/diy/framework/web/mvc/view/HtmlViewResolver.java b/src/main/java/com/diy/framework/web/mvc/view/HtmlViewResolver.java new file mode 100644 index 00000000..f03de524 --- /dev/null +++ b/src/main/java/com/diy/framework/web/mvc/view/HtmlViewResolver.java @@ -0,0 +1,10 @@ +package com.diy.framework.web.mvc.view; + +public class HtmlViewResolver implements ViewResolver { + + @Override + public View resolveViewName(final String viewName) { + final String resolvedViewName = "/templates/" + viewName + ".html"; + return new HtmlView(resolvedViewName); + } +} diff --git a/src/main/java/com/diy/framework/web/mvc/view/JspView.java b/src/main/java/com/diy/framework/web/mvc/view/JspView.java new file mode 100644 index 00000000..81aec511 --- /dev/null +++ b/src/main/java/com/diy/framework/web/mvc/view/JspView.java @@ -0,0 +1,26 @@ +package com.diy.framework.web.mvc.view; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Map; + +public class JspView implements View { + private final String viewName; + + public JspView(final String viewName) { + this.viewName = viewName; + } + + @Override + public void render(final Map model, final HttpServletRequest req, final HttpServletResponse res) throws ServletException, IOException { + for (final Map.Entry entry : model.entrySet()) { + req.setAttribute(entry.getKey(), entry.getValue()); + } + + final RequestDispatcher requestDispatcher = req.getRequestDispatcher(viewName); + requestDispatcher.forward(req, res); + } +} diff --git a/src/main/java/com/diy/framework/web/mvc/view/JspViewResolver.java b/src/main/java/com/diy/framework/web/mvc/view/JspViewResolver.java new file mode 100644 index 00000000..4c024917 --- /dev/null +++ b/src/main/java/com/diy/framework/web/mvc/view/JspViewResolver.java @@ -0,0 +1,9 @@ +package com.diy.framework.web.mvc.view; + +public class JspViewResolver implements ViewResolver { + + @Override + public View resolveViewName(final String viewName) { + return new JspView("/" + viewName + ".jsp"); + } +} diff --git a/src/main/java/com/diy/framework/web/mvc/view/ModelAndView.java b/src/main/java/com/diy/framework/web/mvc/view/ModelAndView.java new file mode 100644 index 00000000..bff5ecf3 --- /dev/null +++ b/src/main/java/com/diy/framework/web/mvc/view/ModelAndView.java @@ -0,0 +1,27 @@ +package com.diy.framework.web.mvc.view; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class ModelAndView { + private final String viewName; + private final Map model = new HashMap<>(); + + public ModelAndView(final String viewName) { + this.viewName = viewName; + } + + public ModelAndView(final String viewName, final Map model) { + this.viewName = viewName; + this.model.putAll(model); + } + + public String getViewName() { + return viewName; + } + + public Map getModel() { + return Collections.unmodifiableMap(this.model); + } +} diff --git a/src/main/java/com/diy/framework/web/mvc/view/RedirectView.java b/src/main/java/com/diy/framework/web/mvc/view/RedirectView.java new file mode 100644 index 00000000..680298e0 --- /dev/null +++ b/src/main/java/com/diy/framework/web/mvc/view/RedirectView.java @@ -0,0 +1,19 @@ +package com.diy.framework.web.mvc.view; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +public class RedirectView implements View { + + private final String redirectUrl; + + public RedirectView(final String redirectUrl) { + this.redirectUrl = redirectUrl; + } + + @Override + public void render(final Map model, final HttpServletRequest req, final HttpServletResponse res) throws Exception { + res.sendRedirect(redirectUrl); + } +} diff --git a/src/main/java/com/diy/framework/web/mvc/view/UrlBasedViewResolver.java b/src/main/java/com/diy/framework/web/mvc/view/UrlBasedViewResolver.java new file mode 100644 index 00000000..32950bb6 --- /dev/null +++ b/src/main/java/com/diy/framework/web/mvc/view/UrlBasedViewResolver.java @@ -0,0 +1,13 @@ +package com.diy.framework.web.mvc.view; + +public class UrlBasedViewResolver implements ViewResolver { + @Override + public View resolveViewName(final String viewName) { + if (!viewName.startsWith("redirect:")) { + return null; + } + + final String redirectUrl = viewName.substring("redirect:".length()); + return new RedirectView(redirectUrl); + } +} diff --git a/src/main/java/com/diy/framework/web/mvc/view/View.java b/src/main/java/com/diy/framework/web/mvc/view/View.java new file mode 100644 index 00000000..5a4e1287 --- /dev/null +++ b/src/main/java/com/diy/framework/web/mvc/view/View.java @@ -0,0 +1,9 @@ +package com.diy.framework.web.mvc.view; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +public interface View { + void render(final Map model, final HttpServletRequest req, final HttpServletResponse res) throws Exception; +} diff --git a/src/main/java/com/diy/framework/web/mvc/view/ViewResolver.java b/src/main/java/com/diy/framework/web/mvc/view/ViewResolver.java new file mode 100644 index 00000000..c7ecd211 --- /dev/null +++ b/src/main/java/com/diy/framework/web/mvc/view/ViewResolver.java @@ -0,0 +1,5 @@ +package com.diy.framework.web.mvc.view; + +public interface ViewResolver { + View resolveViewName(final String viewName); +} diff --git a/src/main/java/com/diy/framework/web/server/TomcatWebServer.java b/src/main/java/com/diy/framework/web/server/TomcatWebServer.java index 47dbb0e7..fc0663ae 100644 --- a/src/main/java/com/diy/framework/web/server/TomcatWebServer.java +++ b/src/main/java/com/diy/framework/web/server/TomcatWebServer.java @@ -2,10 +2,12 @@ import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; +import org.apache.catalina.Wrapper; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.webresources.DirResourceSet; import org.apache.catalina.webresources.StandardRoot; +import javax.servlet.http.HttpServlet; import java.io.File; import java.net.URISyntaxException; import java.nio.file.Paths; @@ -16,6 +18,11 @@ public class TomcatWebServer { private final Tomcat tomcat = new Tomcat(); private final int port = 8080; + private final HttpServlet servlet; + + public TomcatWebServer(final HttpServlet servlet) { + this.servlet = servlet; + } public void start() { setServerContext(); @@ -44,6 +51,12 @@ private void setServerContext() { context.setResponseCharacterEncoding("UTF-8"); setServerResources(context); + setDispatcherServlet(context); + } + + private void setDispatcherServlet(final Context context) { + final Wrapper sw = this.tomcat.addServlet(context.getPath(), "dispatcherServlet", servlet); + sw.addMapping("/"); } private void setServerResources(final Context context) { diff --git a/src/main/java/com/diy/framework/web/servlet/DispatcherServlet.java b/src/main/java/com/diy/framework/web/servlet/DispatcherServlet.java new file mode 100644 index 00000000..cbe989b8 --- /dev/null +++ b/src/main/java/com/diy/framework/web/servlet/DispatcherServlet.java @@ -0,0 +1,86 @@ +package com.diy.framework.web.servlet; + +import com.diy.framework.web.mvc.Controller; +import com.diy.framework.web.mvc.view.JspViewResolver; +import com.diy.framework.web.mvc.view.ModelAndView; +import com.diy.framework.web.mvc.view.UrlBasedViewResolver; +import com.diy.framework.web.mvc.view.View; +import com.diy.framework.web.mvc.view.ViewResolver; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@WebServlet("/") +public class DispatcherServlet extends HttpServlet { + + private final Map controllersMapping; + private final List viewResolvers = new ArrayList<>(); + + public DispatcherServlet(final Map controllersMapping) { + this.controllersMapping = controllersMapping; + this.viewResolvers.add(new UrlBasedViewResolver()); + this.viewResolvers.add(new JspViewResolver()); + } + + @Override + protected void service(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { + final String uri = req.getRequestURI(); + + final Controller controller = controllersMapping.get(uri); + + if (controller == null) { + return; + } + + try { + final ModelAndView mav = controller.handleRequest(req, resp); + render(mav, req, resp); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void render(final ModelAndView mav, final HttpServletRequest req, final HttpServletResponse resp) throws Exception { + final String viewName = mav.getViewName(); + + final View view = resolveViewName(viewName); + + if (view == null) { + throw new RuntimeException("View not found: " + viewName); + } + + view.render(mav.getModel(), req, resp); + } + + private View resolveViewName(final String viewName) { + for (final ViewResolver viewResolver : this.viewResolvers) { + final View view = viewResolver.resolveViewName(viewName); + if (view != null) { + return view; + } + } + + return null; + } + + private Map parseParams(final HttpServletRequest req) throws IOException { + if ("application/json".equals(req.getHeader("Content-Type"))) { + final byte[] bodyBytes = req.getInputStream().readAllBytes(); + final String body = new String(bodyBytes, StandardCharsets.UTF_8); + + return new ObjectMapper().readValue(body, new TypeReference>() {}); + } else { + return req.getParameterMap(); + } + } +}