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/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..2847fe29 --- /dev/null +++ b/src/main/java/com/diy/app/LectureApplication.java @@ -0,0 +1,20 @@ +package com.diy.app; + +import com.diy.framework.web.mvc.Controller; +import com.diy.framework.web.server.TomcatWebServer; +import com.diy.framework.web.servlet.DispatcherServlet; + +import java.util.HashMap; +import java.util.Map; + +public class LectureApplication { + public static void main(String[] args) { + final Map controllerMapping = new HashMap<>(); + final LectureRepository lectureRepository = new LectureMapRepositoryImpl(); + controllerMapping.put("/lectures", new LectureController(lectureRepository)); + + 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..6a0912d0 --- /dev/null +++ b/src/main/java/com/diy/app/LectureController.java @@ -0,0 +1,57 @@ +package com.diy.app; + +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; + +public class LectureController implements Controller { + +// private final Map lectureRepository = new HashMap<>(); + private final LectureRepository lectureRepository; + + public LectureController(final LectureRepository lectureRepository) { + this.lectureRepository = lectureRepository; + } + + @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/LectureMapRepositoryImpl.java b/src/main/java/com/diy/app/LectureMapRepositoryImpl.java new file mode 100644 index 00000000..85551275 --- /dev/null +++ b/src/main/java/com/diy/app/LectureMapRepositoryImpl.java @@ -0,0 +1,52 @@ +package com.diy.app; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; + +public class LectureMapRepositoryImpl implements LectureRepository{ + ConcurrentHashMap lectureMap = new ConcurrentHashMap<>(); + + @Override + public Lecture findById(Long id) { + return lectureMap.getOrDefault(id, null); + } + + @Override + public void put(Long id, Lecture lecture) { + lectureMap.put(id, lecture); + } + + @Override + public void remove(Long id) { + lectureMap.remove(id); + } + + @Override + public void update(Lecture lecture) { + lectureMap.put(lecture.getId(), lecture); + } + + @Override + public long size() { + return lectureMap.size(); + } + + @Override + public Collection values() { + return lectureMap.values(); + } + + @Override + public Collection values(int start, int end) { + if (start > end) { + throw new IllegalArgumentException("start must be less than end"); + } + + ArrayList lectures = new ArrayList<>(end - start + 1); + for (int i = 0; i < end - start + 1; i++) { + lectures.add(lectureMap.get(start + i)); + } + return lectures; + } +} diff --git a/src/main/java/com/diy/app/LectureRepository.java b/src/main/java/com/diy/app/LectureRepository.java new file mode 100644 index 00000000..1835c2e2 --- /dev/null +++ b/src/main/java/com/diy/app/LectureRepository.java @@ -0,0 +1,13 @@ +package com.diy.app; + +import java.util.Collection; + +public interface LectureRepository { + public Lecture findById(Long id); + public void put(Long id, Lecture lecture); + public void remove(Long id); + public void update(Lecture lecture); + public long size(); + public Collection values(); + public Collection values(int start, int end); +} 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/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(); + } + } +}