diff --git a/README.md b/README.md
deleted file mode 100644
index ce6b0d2..0000000
--- a/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-# sample
-Sample Project For Demostration of SpringBoot Only
diff --git a/nb-configuration.xml b/nb-configuration.xml
deleted file mode 100644
index e4cf0f1..0000000
--- a/nb-configuration.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
- Tomcat
- /less:/css
- false
- false
-
-
- /scss:/css
- false
- 1.7-web
-
-
diff --git a/nbactions.tmp b/nbactions.tmp
deleted file mode 100644
index e69de29..0000000
diff --git a/src/main/java/com/hoelee/demo/demo/DemoApplication.java b/src/main/java/com/hoelee/demo/demo/DemoApplication.java
deleted file mode 100644
index f6678f1..0000000
--- a/src/main/java/com/hoelee/demo/demo/DemoApplication.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.hoelee.demo.demo;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.ApplicationContext;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.ComponentScan;
-
-import org.thymeleaf.TemplateEngine;
-import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
-import org.thymeleaf.templateresolver.DefaultTemplateResolver;
-import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
-
-/**
- *
Class desc:
- *
- *
- * @version v2, 2020-09-30 12:01:49AM
- * @author hoelee
- */
-@SpringBootApplication
-@EnableAutoConfiguration
-@EnableConfigurationProperties
-@ComponentScan({"com.hoelee.demo.demo.controller", "com.hoelee.demo.demo.helper"})
-public class DemoApplication {
-
- private static TemplateEngine templateEngine;
-
- /** hoelee v2 2020-09-30 12:01:49AM
- * Method desc:
- *
- *
- * @param args
- */
- public static void main(String[] args) {
- SpringApplication springApplication = new SpringApplication();
- ApplicationContext appCtx = springApplication.run(DemoApplication.class, args);
- }
- /** hoelee v2 2020-09-30 03:54:52AM
- * Method desc:
- *
- @Bean
- public TemplateResolver templateResolver() {
- TemplateResolver templateResolver = new ClassLoaderTemplateResolver();
-
- //templateResolver.setCacheable(false);
- //templateResolver.setTemplateMode("XHTML"); // Default
- templateResolver.setPrefix("/WEB-INF/thymeleaf/");
- templateResolver.setCacheTTLMs(3600000L);
- templateResolver.setSuffix(".html");
- templateResolver.setTemplateMode("HTML5");
- return templateResolver;
- }
-
- * @return
- private static void initializeTemplateEngine() {
- ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver();
-
- // XHTML is the default mode, but we set it anyway for better understanding of code
- templateResolver.setTemplateMode("XHTML");
-
- // This will convert "home" to "/WEB-INF/templates/home.html"
- templateResolver.setPrefix("/WEB-INF/thymeleaf/");
- templateResolver.setSuffix(".html");
-
- // Template cache TTL=1h. If not set, entries would be cached until expelled by LRU
- templateResolver.setCacheTTLMs(3600000L);
-
- //templateResolver.setCacheable(false);
- templateEngine = new TemplateEngine();
-
- templateEngine.setTemplateResolver(templateResolver);
- }
- */
-}
-
-//~ v2, 2020-09-30 05:26:41AM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/demo/demo/MvcConfig.java b/src/main/java/com/hoelee/demo/demo/MvcConfig.java
deleted file mode 100644
index b5a932d..0000000
--- a/src/main/java/com/hoelee/demo/demo/MvcConfig.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.hoelee.demo.demo;
-
-import org.springframework.beans.BeansException;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.ApplicationContext;
-import org.springframework.context.ApplicationContextAware;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.core.Ordered;
-import org.springframework.web.servlet.ViewResolver;
-import org.springframework.web.servlet.config.annotation.EnableWebMvc;
-import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-import org.springframework.web.servlet.view.InternalResourceViewResolver;
-import org.springframework.web.servlet.view.JstlView;
-
-import org.thymeleaf.TemplateEngine;
-import org.thymeleaf.spring5.ISpringTemplateEngine;
-import org.thymeleaf.spring5.SpringTemplateEngine;
-import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
-import org.thymeleaf.spring5.view.ThymeleafViewResolver;
-import org.thymeleaf.templatemode.TemplateMode;
-import org.thymeleaf.templateresolver.ITemplateResolver;
-import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
-
-import javax.servlet.ServletContext;
-
-/**
- * Class desc:
- *
- *
- * @version v2, 2020-09-30 05:59:17AM
- * @author hoelee
- */
-@Configuration
-@EnableWebMvc
-public class MvcConfig {
-
- @Autowired
- private TemplateEngine templateEngine;
- @Autowired
- private ServletContext servletContext;
-
- @Bean
- private void initializeTemplateEngine() {
- ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(servletContext);
-
- // HTML is the default mode, but we set it anyway for better understanding of code
- templateResolver.setTemplateMode(TemplateMode.HTML);
-
- // This will convert "home" to "/WEB-INF/templates/home.html"
- templateResolver.setPrefix("/WEB-INF/thymeleaf/");
- templateResolver.setSuffix(".html");
-
- // Template cache TTL=1h. If not set, entries would be cached until expelled
- templateResolver.setCacheTTLMs(Long.valueOf(3600000L));
-
- // Cache is set to true by default. Set to false if you want templates to
- // be automatically updated when modified.
- templateResolver.setCacheable(true);
-
- this.templateEngine = new TemplateEngine();
-
- this.templateEngine.setTemplateResolver(templateResolver);
- }
-}
-
-//~ v2, 2020-09-30 05:59:17AM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/demo/demo/ServletInitializer.java b/src/main/java/com/hoelee/demo/demo/ServletInitializer.java
deleted file mode 100644
index 28975f6..0000000
--- a/src/main/java/com/hoelee/demo/demo/ServletInitializer.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.hoelee.demo.demo;
-
-import org.springframework.boot.builder.SpringApplicationBuilder;
-import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
-
-public class ServletInitializer extends SpringBootServletInitializer {
-
- @Override
- protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
- return application.sources(DemoApplication.class);
- }
-}
diff --git a/src/main/java/com/hoelee/demo/demo/controller/HomeController.java b/src/main/java/com/hoelee/demo/demo/controller/HomeController.java
deleted file mode 100644
index 4a8ce96..0000000
--- a/src/main/java/com/hoelee/demo/demo/controller/HomeController.java
+++ /dev/null
@@ -1,434 +0,0 @@
-package com.hoelee.demo.demo.controller;
-
-import com.hoelee.demo.demo.entity.Comment;
-import com.hoelee.demo.demo.entity.Post;
-import com.hoelee.demo.demo.helper.InternetHelper;
-import com.hoelee.demo.demo.helper.ListHelper;
-
-import okhttp3.Authenticator;
-import okhttp3.Call;
-import okhttp3.Cookie;
-import okhttp3.CookieJar;
-import okhttp3.Credentials;
-import okhttp3.HttpUrl;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-import okhttp3.Route;
-
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.ResponseBody;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.io.IOException;
-
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Class desc:
- *
- * @version v2, 2020-09-30 03:02:39AM
- * @author hoelee
- */
-@Controller
-@RequestMapping(value = "", method = {RequestMethod.GET, RequestMethod.POST})
-public class HomeController {
-
- private final Logger log = LoggerFactory.getLogger(this.getClass());
-
- //
- @Autowired
- private InternetHelper internetHelper;
-
- @ResponseBody
- @GetMapping({"", "/"})
- private String home() {
- return "Hello World";
- }
-
- @ResponseBody
- @GetMapping("/post/topComment")
- private String question1TopCommentEndPoint() {
- List postList = internetHelper.readAllPosts();
- List commentList = internetHelper.readAllComments();
-
- // Distribute respective Comment into each Post
- for(int a = 0; a < postList.size(); a++) {
- Post post = postList.get(a);
- List commentListCurrentPost = new LinkedList<>();
-
- for(int b = 0; b < commentList.size(); b++) {
- Comment comment = commentList.get(b);
-
- if (comment.getPostId() == post.getId())
- commentListCurrentPost.add(comment);
- }
-
- post.setCommentList(commentListCurrentPost);
- }
-
- // Sort Post by highest number of comment
- Collections.sort(postList,
- new Comparator() {
-
- @Override
- public int compare(Post post1, Post post2) {
- int post1CommentSize = post1.getCommentList().size();
- int post2CommentSize = post2.getCommentList().size();
-
- return post2CommentSize - post1CommentSize;
- }
-
- });
-
- // Response JSONArray
- JSONArray root = new JSONArray();
-
- for(int a = 0; a < postList.size(); a++) {
- root.put(postList.get(a).toJSONObjectCustomResponse1());
- }
-
- return root.toString();
- }
-
- /** hoelee v2 2020-10-02 12:40:33PM
- * Method desc:
- *
- *
- * @param postId
- * @param commentId
- * @param name
- * @param email
- * @param body
- * @param operator - provide 'and', 'or'
- * @return
- */
- @ResponseBody
- @GetMapping("/search")
- private String question2SearchComment( //
- @RequestParam(value = "postId", required = false) String postId, //
- @RequestParam(value = "commentId", required = false) String commentId, //
- @RequestParam(value = "name", required = false) String name, //
- @RequestParam(value = "email", required = false) String email, //
- @RequestParam(value = "body", required = false) String body, //
- @RequestParam(value = "operator", required = false) String operator //
- ) {
- List commentList = internetHelper.readAllComments();
-
- // Default no filter handler
- if ((postId == null) && (commentId == null) && (name == null) && (email == null) && (body == null)) {
-
- // Response JSONArray
- JSONArray root = new JSONArray();
-
- for(int a = 0; a < commentList.size(); a++) {
- root.put(commentList.get(a).toJSONObject());
- }
-
- return root.toString();
- }
-
- // Check operator
- boolean operatorAnd = false;
- boolean operatorOr = false;
-
- if (!((operator == null) || operator.isEmpty())) {
- operator = operator.toLowerCase();
-
- if (operator == "and")
- operatorAnd = true;
- if (operator == "or")
- operatorOr = true;
- } else {
-
- // Default Operator
- operatorOr = true;
- }
-
- // Start the search
- if (operatorOr) {
-
- // Do operator OR
- List commentListPostId = new LinkedList<>();
- List commnetListCommentId = new LinkedList<>();
- List commentListName = new LinkedList<>();
- List commentListEmail = new LinkedList<>();
- List commentListBody = new LinkedList<>();
- List commentListResult = new LinkedList<>();
-
- if (!((postId == null) || postId.isEmpty())) {
-
- // Remove unrelated post id
- for(int a = 0; a < commentList.size(); a++) {
- Comment comment = commentList.get(a);
- int postIdInt = Integer.parseInt(postId);
-
- if (comment.getPostId() == postIdInt)
- commentListPostId.add(comment);
- }
- }
-
- if (!((commentId == null) || commentId.isEmpty())) {
-
- // Remove unrelated comment id
- for(int a = 0; a < commentList.size(); a++) {
- Comment comment = commentList.get(a);
- int commentIdInt = Integer.parseInt(commentId);
-
- if (comment.getId() == commentIdInt)
- commnetListCommentId.add(comment);
- }
- }
-
- if (!((name == null) || name.isEmpty())) {
-
- // Remove unrelated comment id
- for(int a = 0; a < commentList.size(); a++) {
- Comment comment = commentList.get(a);
-
- if (comment.getName().contains(name))
- commentListName.add(comment);
- }
- }
-
- if (!((email == null) || email.isEmpty())) {
-
- // Remove unrelated comment id
- for(int a = 0; a < commentList.size(); a++) {
- Comment comment = commentList.get(a);
-
- if (comment.getEmail().contains(email))
- commentListEmail.add(comment);
- }
- }
-
- if (!((body == null) || body.isEmpty())) {
-
- // Remove unrelated comment id
- for(int a = 0; a < commentList.size(); a++) {
- Comment comment = commentList.get(a);
-
- if (comment.getBody().contains(body))
- commentListBody.add(comment);
- }
- }
-
- // Remove duplicate Comment
- Set commentSetResult = new HashSet<>();
-
- commentListResult.addAll(commentListPostId);
- commentListResult.addAll(commnetListCommentId);
- commentListResult.addAll(commentListName);
- commentListResult.addAll(commentListEmail);
- commentListResult.addAll(commentListBody);
-
- for(int a = 0; a < commentListResult.size(); a++) {
- commentSetResult.add(commentListResult.get(a));
- }
-
- // Response JSONArray
- JSONArray root = new JSONArray();
-
- for(Comment cmt : commentSetResult) {
- root.put(cmt.toJSONObject());
- }
-
- return root.toString();
- } else {
-
- // Do operator AND
- List commentTotal = new LinkedList<>();
-
- if (!((postId == null) || postId.isEmpty())) {
- List commentListResult = new LinkedList<>();
-
- // Remove unrelated post id
- for(int a = 0; a < commentList.size(); a++) {
- Comment comment = commentList.get(a);
- int postIdInt = Integer.parseInt(postId);
-
- if (comment.getPostId() == postIdInt)
- commentListResult.add(comment);
- }
-
- // Find out union result save to commentSetTotal
- if (commentTotal.isEmpty()) {
- commentTotal.addAll(commentListResult);
- } else {
- List commentTotalNew = new LinkedList<>();
-
- for(int a = 0; a < commentTotal.size(); a++) {
- Comment cmt1 = commentTotal.get(a);
-
- for(int b = 0; b < commentListResult.size(); b++) {
- Comment cmt2 = commentListResult.get(b);
-
- if (cmt1 == cmt2)
- commentTotalNew.add(cmt2);
- }
- }
-
- commentTotal = commentTotalNew;
- }
- }
-
- if (!((commentId == null) || commentId.isEmpty())) {
- List commentListResult = new LinkedList<>();
-
- // Remove unrelated post id
- for(int a = 0; a < commentList.size(); a++) {
- Comment comment = commentList.get(a);
- int commentIdInt = Integer.parseInt(commentId);
-
- if (comment.getId() == commentIdInt)
- commentListResult.add(comment);
- }
-
- // Find out union result save to commentSetTotal
- if (commentTotal.isEmpty()) {
- commentTotal.addAll(commentListResult);
- } else {
- List commentTotalNew = new LinkedList<>();
-
- for(int a = 0; a < commentTotal.size(); a++) {
- Comment cmt1 = commentTotal.get(a);
-
- for(int b = 0; b < commentListResult.size(); b++) {
- Comment cmt2 = commentListResult.get(b);
-
- if (cmt1 == cmt2)
- commentTotalNew.add(cmt2);
- }
- }
-
- commentTotal = commentTotalNew;
- }
- }
-
- if (!((name == null) || name.isEmpty())) {
- List commentListResult = new LinkedList<>();
-
- // Remove unrelated post id
- for(int a = 0; a < commentList.size(); a++) {
- Comment comment = commentList.get(a);
-
- if (comment.getName().contains(name))
- commentListResult.add(comment);
- }
-
- // Find out union result save to commentSetTotal
- if (commentTotal.isEmpty()) {
- commentTotal.addAll(commentListResult);
- } else {
- List commentTotalNew = new LinkedList<>();
-
- for(int a = 0; a < commentTotal.size(); a++) {
- Comment cmt1 = commentTotal.get(a);
-
- for(int b = 0; b < commentListResult.size(); b++) {
- Comment cmt2 = commentListResult.get(b);
-
- if (cmt1 == cmt2)
- commentTotalNew.add(cmt2);
- }
- }
-
- commentTotal = commentTotalNew;
- }
- }
-
- if (!((email == null) || email.isEmpty())) {
- List commentListResult = new LinkedList<>();
-
- // Remove unrelated post id
- for(int a = 0; a < commentList.size(); a++) {
- Comment comment = commentList.get(a);
-
- if (comment.getEmail().contains(email))
- commentListResult.add(comment);
- }
-
- // Find out union result save to commentSetTotal
- if (commentTotal.isEmpty()) {
- commentTotal.addAll(commentListResult);
- } else {
- List commentTotalNew = new LinkedList<>();
-
- for(int a = 0; a < commentTotal.size(); a++) {
- Comment cmt1 = commentTotal.get(a);
-
- for(int b = 0; b < commentListResult.size(); b++) {
- Comment cmt2 = commentListResult.get(b);
-
- if (cmt1 == cmt2)
- commentTotalNew.add(cmt2);
- }
- }
-
- commentTotal = commentTotalNew;
- }
- }
-
- if (!((body == null) || body.isEmpty())) {
- List commentListResult = new LinkedList<>();
-
- // Remove unrelated post id
- for(int a = 0; a < commentList.size(); a++) {
- Comment comment = commentList.get(a);
-
- if (comment.getBody().contains(body))
- commentListResult.add(comment);
- }
-
- // Find out union result save to commentSetTotal
- if (commentTotal.isEmpty()) {
- commentTotal.addAll(commentListResult);
- } else {
- List commentTotalNew = new LinkedList<>();
-
- for(int a = 0; a < commentTotal.size(); a++) {
- Comment cmt1 = commentTotal.get(a);
-
- for(int b = 0; b < commentListResult.size(); b++) {
- Comment cmt2 = commentListResult.get(b);
-
- if (cmt1 == cmt2)
- commentTotalNew.add(cmt2);
- }
- }
-
- commentTotal = commentTotalNew;
- }
- }
-
- // Response JSONArray
- JSONArray root = new JSONArray();
-
- for(Comment cmt : commentTotal) {
- root.put(cmt.toJSONObject());
- }
-
- return root.toString();
- }
- }
-}
-
-//~ v2, 2020-10-02 03:53:15PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/demo/demo/controller/TestController.java b/src/main/java/com/hoelee/demo/demo/controller/TestController.java
deleted file mode 100644
index 86e7a34..0000000
--- a/src/main/java/com/hoelee/demo/demo/controller/TestController.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package com.hoelee.demo.demo.controller;
-
-import com.hoelee.demo.demo.entity.Comment;
-import com.hoelee.demo.demo.entity.Post;
-import com.hoelee.demo.demo.helper.InternetHelper;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.LinkedList;
-import java.util.List;
-import org.json.JSONArray;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-import org.springframework.web.bind.annotation.ResponseBody;
-
-@Controller
-@RequestMapping(value = "/test", method = {RequestMethod.GET, RequestMethod.POST})
-public class TestController {
-
- private final Logger log = LoggerFactory.getLogger(this.getClass());
-
- //
- @Autowired
- private InternetHelper internetHelper;
-
- @ResponseBody
- @GetMapping({"/1"})
- private String testSorting() {
- List commentList = internetHelper.readAllComments();
- List postList = new LinkedList<>();
- Post post1 = new Post();
- List commentList1 = new LinkedList<>();
-
- commentList1.add(new Comment());
- commentList1.add(new Comment());
- commentList1.add(new Comment());
- post1.setCommentList(commentList1);
-
- Post post2 = new Post();
- List commentList2 = new LinkedList<>();
-
- commentList2.add(new Comment());
- commentList2.add(new Comment());
- post2.setCommentList(commentList2);
-
- Post post3 = new Post();
- List commentList3 = new LinkedList<>();
-
- commentList3.add(new Comment());
- post3.setCommentList(commentList3);
- postList.add(post1);
- postList.add(post2);
- postList.add(post3);
- Collections.sort(postList,
- new Comparator() {
-
- @Override
- public int compare(Post post1, Post post2) {
- int post1CommentSize = post1.getCommentList().size();
- int post2CommentSize = post2.getCommentList().size();
-
- return post2CommentSize - post1CommentSize;
- }
-
- });
-
- JSONArray root = new JSONArray();
-
- for(int a = 0; a < postList.size(); a++) {
- root.put(postList.get(a).toJSONObjectCustomResponse1());
- }
-
- return root.toString();
- }
-
- @GetMapping("/2")
- private String testThymeleaf() {
- return "testing.html";
- }
-}
diff --git a/src/main/java/com/hoelee/demo/demo/entity/Comment.java b/src/main/java/com/hoelee/demo/demo/entity/Comment.java
deleted file mode 100644
index c8ba4f9..0000000
--- a/src/main/java/com/hoelee/demo/demo/entity/Comment.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package com.hoelee.demo.demo.entity;
-
-import com.hoelee.demo.demo.exception.ExceptionJSONConversion;
-import org.json.JSONObject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Class desc:
- *
- *
- * @version v2, 2020-10-01 06:30:52PM
- * @author hoelee
- */
-public class Comment {
-
- private final Logger log = LoggerFactory.getLogger(this.getClass());
- private int postId;
- private int id;
- private String name;
- private String email;
- private String body;
-
- public Comment(){
- }
-
- public Comment(int postId, int id, String name, String email, String body) {
- this.postId = postId;
- this.id = id;
- this.name = name;
- this.email = email;
- this.body = body;
- }
-
- public int getPostId() {
- return postId;
- }
-
- public void setPostId(int postId) {
- this.postId = postId;
- }
-
- public int getId() {
- return id;
- }
-
- public void setId(int id) {
- this.id = id;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public String getEmail() {
- return email;
- }
-
- public void setEmail(String email) {
- this.email = email;
- }
-
- public String getBody() {
- return body;
- }
-
- public void setBody(String body) {
- this.body = body;
- }
-
- public void fromJSONObject(JSONObject jo) throws Exception{
- try{
- if(jo.has("postId"))
- postId = jo.getInt("postId");
- if (jo.has("id"))
- id = jo.getInt("id");
- if (jo.has("name"))
- name = jo.getString("name");
- if (jo.has("email"))
- email = jo.getString("email");
- if (jo.has("body"))
- body = jo.getString("body");
- }catch(Exception ex){
- log.error(ex.getLocalizedMessage());
- throw new ExceptionJSONConversion(ex.getLocalizedMessage());
- }
- }
-
- public JSONObject toJSONObject(){
- JSONObject jo = new JSONObject();
- jo.put("postId", postId);
- jo.put("id", id);
- jo.put("name", name);
- jo.put("email", email);
- jo.put("body", body);
- return jo;
- }
-
-}
-
-//~ v2, 2020-10-01 06:30:52PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/demo/demo/entity/Post.java b/src/main/java/com/hoelee/demo/demo/entity/Post.java
deleted file mode 100644
index 3c59321..0000000
--- a/src/main/java/com/hoelee/demo/demo/entity/Post.java
+++ /dev/null
@@ -1,199 +0,0 @@
-package com.hoelee.demo.demo.entity;
-
-import com.hoelee.demo.demo.exception.ExceptionJSONConversion;
-
-import org.json.JSONObject;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.LinkedList;
-import java.util.List;
-
-/**
- * Class desc:
- *
- *
- * @version v2, 2020-10-02 11:47:11AM
- * @author hoelee
- */
-public class Post {
-
- private final Logger log = LoggerFactory.getLogger(this.getClass());
- // All the comments for this post
- private List commentList = new LinkedList<>();
- private int userId;
- private int id;
- private String title;
- private String body;
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Constructor desc:
- *
- */
- public Post() {}
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Constructor desc:
- *
- *
- * @param userId
- * @param id
- * @param title
- * @param body
- */
- public Post(int userId, int id, String title, String body) {
- this.userId = userId;
- this.id = id;
- this.title = title;
- this.body = body;
- }
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Method desc:
- *
- * @return
- */
- public int getUserId() {
- return userId;
- }
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Method desc:
- *
- *
- * @param userId
- */
- public void setUserId(int userId) {
- this.userId = userId;
- }
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Method desc:
- *
- * @return
- */
- public int getId() {
- return id;
- }
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Method desc:
- *
- *
- * @param id
- */
- public void setId(int id) {
- this.id = id;
- }
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Method desc:
- *
- * @return
- */
- public String getTitle() {
- return title;
- }
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Method desc:
- *
- *
- * @param title
- */
- public void setTitle(String title) {
- this.title = title;
- }
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Method desc:
- *
- * @return
- */
- public String getBody() {
- return body;
- }
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Method desc:
- *
- *
- * @param body
- */
- public void setBody(String body) {
- this.body = body;
- }
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Method desc:
- * All the comments for this post
- *
- * @return
- */
- public List getCommentList() {
- return commentList;
- }
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Method desc:
- * All the comments for this post
- *
- * @param commentList
- */
- public void setCommentList(List commentList) {
- this.commentList = commentList;
- }
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Method desc:
- *
- *
- * @param jo
- *
- * @throws Exception
- */
- public void fromJSONObject(JSONObject jo) throws Exception {
- try {
- if (jo.has("userId"))
- userId = jo.getInt("userId");
- if (jo.has("id"))
- id = jo.getInt("id");
- if (jo.has("title"))
- title = jo.getString("title");
- if (jo.has("body"))
- body = jo.getString("body");
- } catch (Exception ex) {
- log.error(ex.getLocalizedMessage());
- throw new ExceptionJSONConversion(ex.getLocalizedMessage());
- }
- }
-
- /** hoelee v2 2020-10-02 11:47:11AM
- * Method desc:
- *
- * @return
- */
- public JSONObject toJSONObject() {
- JSONObject jo = new JSONObject();
-
- jo.put("userId", userId);
- jo.put("id", id);
- jo.put("title", title);
- jo.put("body", body);
- return jo;
- }
-
- public JSONObject toJSONObjectCustomResponse1(){
- JSONObject jo = new JSONObject();
-
- jo.put("post_id", id);
- jo.put("post_title", title);
- jo.put("post_body", body);
- jo.put("total_number_of_comments", commentList.size());
- return jo;
- }
-
-}
-
-//~ v2, 2020-10-02 11:47:11AM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/demo/demo/exception/ExceptionJSONConversion.java b/src/main/java/com/hoelee/demo/demo/exception/ExceptionJSONConversion.java
deleted file mode 100644
index 8d9424b..0000000
--- a/src/main/java/com/hoelee/demo/demo/exception/ExceptionJSONConversion.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.hoelee.demo.demo.exception;
-
-/**
- * Class desc:
- * Handle own JSONObject & JSONArray convert to object
- *
- * @version v2, 2020-10-01 06:39:32PM
- * @author hoelee
- */
-public class ExceptionJSONConversion extends Exception {
-
- private Class modelClass;
-
- /** hoelee v2 2020-10-01 06:39:32PM
- * Constructor desc:
- *
- *
- * @param message
- */
- public ExceptionJSONConversion(String message) {
- super(message);
-
- this.modelClass = modelClass;
- }
-
- /** hoelee v2 2020-10-01 06:39:32PM
- * Method desc:
- *
- * @return
- */
- public Class getModelClass() {
- return modelClass;
- }
-}
-
-//~ v2, 2020-10-01 06:39:32PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/demo/demo/helper/InternetHelper.java b/src/main/java/com/hoelee/demo/demo/helper/InternetHelper.java
deleted file mode 100644
index 47b9c2f..0000000
--- a/src/main/java/com/hoelee/demo/demo/helper/InternetHelper.java
+++ /dev/null
@@ -1,222 +0,0 @@
-package com.hoelee.demo.demo.helper;
-
-import com.hoelee.demo.demo.entity.Comment;
-import com.hoelee.demo.demo.entity.Post;
-
-import okhttp3.Call;
-import okhttp3.Cookie;
-import okhttp3.CookieJar;
-import okhttp3.HttpUrl;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-import java.io.IOException;
-
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import javax.servlet.http.HttpServletRequest;
-
-/**
- * Class desc:
- *
- *
- * @version v2, 2020-09-30 03:14:36AM
- * @author hoelee
- */
-@Component
-public class InternetHelper {
-
- private final Logger log = LoggerFactory.getLogger(this.getClass());
- private HttpServletRequest request;
-
- /** hoelee v2 2020-10-01 06:10:46PM
- * Method desc:
- *
- *
- * @param request
- */
- @Autowired
- public void setRequest(HttpServletRequest request) {
- this.request = request;
- }
-
- /** hoelee v2 2020-10-01 06:10:46PM
- * Method desc:
- *
- * @return
- */
- public List readAllComments() {
- String epReadComments = "https://jsonplaceholder.typicode.com/comments";
- String response = synchronizeRequestGetMethod(epReadComments);
-
- try {
- JSONArray ja = new JSONArray(response);
- List commentList = new LinkedList<>();
-
- for(int a = 0; a < ja.length(); a++) {
- JSONObject jo = ja.getJSONObject(a);
- Comment comment = new Comment();
-
- comment.fromJSONObject(jo);
- commentList.add(comment);
- }
-
- return commentList;
- } catch (Exception ex) {
- log.error(ex.getLocalizedMessage());
- return null;
- }
- }
-
- /** hoelee v2 2020-10-01 06:10:46PM
- * Method desc:
- *
- * @return
- */
- public List readAllPosts() {
- String epReadPosts = "https://jsonplaceholder.typicode.com/posts";
- String response = synchronizeRequestGetMethod(epReadPosts);
-
- try {
- JSONArray ja = new JSONArray(response);
- List postList = new LinkedList<>();
-
- for(int a = 0; a < ja.length(); a++) {
- JSONObject jo = ja.getJSONObject(a);
- Post post = new Post();
-
- post.fromJSONObject(jo);
- postList.add(post);
- }
-
- return postList;
- } catch (Exception ex) {
- log.error(ex.getLocalizedMessage());
- return null;
- }
- }
-
- /** hoelee v2 2020-10-01 06:10:46PM
- * Method desc:
- *
- *
- * @param postId
- * @return
- */
- public Post readPostById(int postId) {
- String epReadPost = "https://jsonplaceholder.typicode.com/posts/" + postId;
- String response = synchronizeRequestGetMethod(epReadPost);
-
- try {
- JSONObject jo = null;
- Post post = new Post();
-
- jo = new JSONObject(response);
-
- post.fromJSONObject(jo);
- return post;
- } catch (Exception ex) {
- log.error(ex.getLocalizedMessage());
- return null;
- }
- }
-
- /** hoelee v2 2020-10-01 06:10:46PM
- * Method desc:
- *
- *
- * @param commentId
- * @return
- */
- public Comment readCommentById(int commentId) {
- String epReadComment = "https://jsonplaceholder.typicode.com/comments/" + commentId;
- String response = synchronizeRequestGetMethod(epReadComment);
-
- try {
- JSONObject jo = new JSONObject(response);
- Comment comment = new Comment();
-
- comment.fromJSONObject(jo);
- return comment;
- } catch (Exception ex) {
- log.error(ex.getLocalizedMessage());
- return null;
- }
- }
-
- /** hoelee v2 2020-09-30 03:14:36AM
- * Method desc:
- *
- *
- * @param url
- * @return
- */
- private String synchronizeRequestGetMethod(String url) {
- CookieJar cookieJar = new CookieJar() {
-
- @Override
- public void saveFromResponse(HttpUrl url, List cookies) {
-
- // Save Cookies
- String urlString = url.toString();
-
- for(Cookie cookie : cookies) {
- String cookieString = cookie.toString();
- }
- }
- @Override
- public List loadForRequest(HttpUrl url) {
-
- // Load new cookies
- ArrayList cookies = new ArrayList<>();
- Cookie cookie = new Cookie.Builder().hostOnlyDomain(url.host()).name("key").value("value").build();
-
- cookies.add(cookie);
- return cookies;
- }
- };
- HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
-
- // Add Parameters
- //urlBuilder.addQueryParameter("key", "value");
- String finalUrl = urlBuilder.build().toString();
- OkHttpClient mOkHttpClient = new OkHttpClient.Builder() //
- .connectTimeout(60, TimeUnit.SECONDS).cookieJar(cookieJar).build();
- Request request = new Request.Builder().url(finalUrl).build();
- Call call = mOkHttpClient.newCall(request);
- Response response = null;
- String body = null;
-
- try {
- response = call.execute();
-
- if (response.isSuccessful())
- body = response.body().string();
- else
- log.warn("Http Get Request not success with URL: " + url);
- } catch (IOException e) {
- log.error("Http GET Request failed with URL: " + url + " because of " + e.getLocalizedMessage());
- } finally {
- try {
- response.close();
- } catch (Exception ex) {}
- }
-
- return body;
- }
-}
-
-//~ v2, 2020-10-02 11:37:44AM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/demo/demo/helper/ListHelper.java b/src/main/java/com/hoelee/demo/demo/helper/ListHelper.java
deleted file mode 100644
index f8c5867..0000000
--- a/src/main/java/com/hoelee/demo/demo/helper/ListHelper.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package com.hoelee.demo.demo.helper;
-
-import com.hoelee.demo.demo.entity.Comment;
-
-import java.util.LinkedList;
-import java.util.List;
-
-/**
- *
- * Class desc:
- *
- * @version v2, 2020-10-02 02:48:54PM
- * @author hoelee
- */
-public class ListHelper {
-
- /**
- * hoelee v2 2020-10-02 02:48:54PM
- *
- * Method desc:
- * Search same Comment in give 2 Comment LinkedList
- *
- * @param l1
- * @param l2
- * @return
- */
- public static List findSameCommentInList(List l1, List l2) {
- // If l2 empty no need find
- if (l2.isEmpty())
- return l1;
-
- List result = new LinkedList<>();
-
- for (int a = 0; a < l1.size(); a++) {
- Comment l1Obj = (Comment) l1.get(a);
- boolean gotSame = false;
-
- for (int b = 0; b < l2.size(); b++) {
- Comment l2Obj = (Comment) l2.get(b);
-
- if (l1Obj == l2Obj) {
- gotSame = true;
- break;
- }
- }
-
- if (gotSame) {
- result.add(l1Obj);
- }
- }
-
- return result;
- }
-}
-
-//~ v2, 2020-10-02 02:48:54PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/config/CacheConfig.java b/src/main/java/com/hoelee/jsonplaceholder/config/CacheConfig.java
new file mode 100644
index 0000000..65dbe72
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/config/CacheConfig.java
@@ -0,0 +1,30 @@
+package com.hoelee.jsonplaceholder.config;
+
+import com.github.benmanes.caffeine.cache.Caffeine;
+import java.time.Duration;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.caffeine.CaffeineCacheManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @version v1, 2024-10-22 08:10:00PM
+ * @author hoelee
+ * Learning note: a bounded local cache improves hot read latency without making cached data permanent.
+ */
+@Configuration
+@EnableCaching
+public class CacheConfig {
+
+ @Bean
+ CacheManager cacheManager() {
+ CaffeineCacheManager cacheManager = new CaffeineCacheManager("posts");
+ cacheManager.setCaffeine(Caffeine.newBuilder()
+ .maximumSize(500)
+ .expireAfterWrite(Duration.ofMinutes(10)));
+ return cacheManager;
+ }
+}
+
+//~ v1, 2024-10-22 08:10:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/config/RestClientConfig.java b/src/main/java/com/hoelee/jsonplaceholder/config/RestClientConfig.java
new file mode 100644
index 0000000..db82e73
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/config/RestClientConfig.java
@@ -0,0 +1,32 @@
+package com.hoelee.jsonplaceholder.config;
+
+import java.net.http.HttpClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.JdkClientHttpRequestFactory;
+import org.springframework.web.client.RestClient;
+
+/**
+ * @version v1, 2024-10-22 09:10:00PM
+ * @author hoelee
+ * Learning note: an explicitly named HTTP client isolates a remote integration from the application API.
+ */
+@Configuration
+public class RestClientConfig {
+
+ @Bean
+ RestClient jsonPlaceholderRestClient(RestClient.Builder builder, ApiProperties properties) {
+ HttpClient httpClient = HttpClient.newBuilder()
+ .connectTimeout(properties.requestTimeout())
+ .build();
+ JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient);
+ requestFactory.setReadTimeout(properties.requestTimeout());
+
+ return builder
+ .baseUrl(properties.jsonPlaceholderBaseUrl())
+ .requestFactory(requestFactory)
+ .build();
+ }
+}
+
+//~ v1, 2024-10-22 09:10:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/config/SecurityConfig.java b/src/main/java/com/hoelee/jsonplaceholder/config/SecurityConfig.java
new file mode 100644
index 0000000..d87e6cd
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/config/SecurityConfig.java
@@ -0,0 +1,52 @@
+package com.hoelee.jsonplaceholder.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+import org.springframework.security.web.SecurityFilterChain;
+
+/**
+ * @version v1, 2024-10-22 10:10:00PM
+ * @author hoelee
+ * Learning note: read operations stay public while state-changing API operations require a role.
+ */
+@Configuration
+public class SecurityConfig {
+
+ @Bean
+ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ return http
+ .csrf(AbstractHttpConfigurer::disable)
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(authorize -> authorize
+ .requestMatchers(HttpMethod.GET, "/api/posts/**", "/actuator/health", "/actuator/info").permitAll()
+ .requestMatchers("/api/**").hasRole("EDITOR")
+ .anyRequest().denyAll())
+ .httpBasic(Customizer.withDefaults())
+ .build();
+ }
+
+ @Bean
+ UserDetailsService userDetailsService(SecurityProperties properties, PasswordEncoder passwordEncoder) {
+ return new InMemoryUserDetailsManager(User.withUsername(properties.editorUsername())
+ .password(passwordEncoder.encode(properties.editorPassword()))
+ .roles("EDITOR")
+ .build());
+ }
+
+ @Bean
+ PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
+
+//~ v1, 2024-10-22 10:10:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/integration/JsonPlaceholderClient.java b/src/main/java/com/hoelee/jsonplaceholder/integration/JsonPlaceholderClient.java
new file mode 100644
index 0000000..65a624f
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/integration/JsonPlaceholderClient.java
@@ -0,0 +1,37 @@
+package com.hoelee.jsonplaceholder.integration;
+
+import com.hoelee.jsonplaceholder.support.UpstreamServiceException;
+import java.util.List;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.client.RestClientException;
+
+/**
+ * @version v1, 2024-10-23 09:15:00PM
+ * @author hoelee
+ * Learning note: RestClient provides a typed boundary where timeouts and remote failures can be handled consistently.
+ */
+@Component
+public class JsonPlaceholderClient {
+
+ private final RestClient restClient;
+
+ public JsonPlaceholderClient(RestClient jsonPlaceholderRestClient) {
+ this.restClient = jsonPlaceholderRestClient;
+ }
+
+ public List fetchPosts(int limit) {
+ try {
+ List posts = restClient.get()
+ .uri("/posts")
+ .retrieve()
+ .body(new ParameterizedTypeReference<>() { });
+ return posts == null ? List.of() : posts.stream().limit(limit).toList();
+ } catch (RestClientException exception) {
+ throw new UpstreamServiceException("JSONPlaceholder could not be reached", exception);
+ }
+ }
+}
+
+//~ v1, 2024-10-23 09:15:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/integration/JsonPlaceholderPost.java b/src/main/java/com/hoelee/jsonplaceholder/integration/JsonPlaceholderPost.java
new file mode 100644
index 0000000..dd09299
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/integration/JsonPlaceholderPost.java
@@ -0,0 +1,11 @@
+package com.hoelee.jsonplaceholder.integration;
+
+/**
+ * @version v1, 2024-10-23 08:15:00PM
+ * @author hoelee
+ * Learning note: a remote DTO is kept separate from the persisted domain model to prevent API coupling.
+ */
+public record JsonPlaceholderPost(Long id, Long userId, String title, String body) {
+}
+
+//~ v1, 2024-10-23 08:15:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/post/CreatePostRequest.java b/src/main/java/com/hoelee/jsonplaceholder/post/CreatePostRequest.java
new file mode 100644
index 0000000..15300ad
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/post/CreatePostRequest.java
@@ -0,0 +1,19 @@
+package com.hoelee.jsonplaceholder.post;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.Size;
+
+/**
+ * @version v1, 2024-10-24 09:20:00PM
+ * @author hoelee
+ * Learning note: immutable request records make the public contract concise and validation rules visible.
+ */
+public record CreatePostRequest(
+ @NotNull @Positive Long authorId,
+ @NotBlank @Size(max = 160) String title,
+ @NotBlank @Size(max = 10_000) String body) {
+}
+
+//~ v1, 2024-10-24 09:20:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/post/ImportResult.java b/src/main/java/com/hoelee/jsonplaceholder/post/ImportResult.java
new file mode 100644
index 0000000..3007835
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/post/ImportResult.java
@@ -0,0 +1,11 @@
+package com.hoelee.jsonplaceholder.post;
+
+/**
+ * @version v1, 2024-10-25 10:25:00PM
+ * @author hoelee
+ * Learning note: import results report what happened without returning an unnecessary full data set.
+ */
+public record ImportResult(int fetched, int imported, int skipped) {
+}
+
+//~ v1, 2024-10-25 10:25:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/post/PageResponse.java b/src/main/java/com/hoelee/jsonplaceholder/post/PageResponse.java
new file mode 100644
index 0000000..6582c00
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/post/PageResponse.java
@@ -0,0 +1,29 @@
+package com.hoelee.jsonplaceholder.post;
+
+import java.util.List;
+import java.util.function.Function;
+import org.springframework.data.domain.Page;
+
+/**
+ * @version v1, 2024-10-25 09:25:00PM
+ * @author hoelee
+ * Learning note: a stable page envelope avoids exposing Spring Data's serialized implementation details.
+ */
+public record PageResponse(
+ List content,
+ int page,
+ int size,
+ long totalElements,
+ int totalPages) {
+
+ public static PageResponse from(Page page, Function mapper) {
+ return new PageResponse<>(
+ page.map(mapper).getContent(),
+ page.getNumber(),
+ page.getSize(),
+ page.getTotalElements(),
+ page.getTotalPages());
+ }
+}
+
+//~ v1, 2024-10-25 09:25:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/post/Post.java b/src/main/java/com/hoelee/jsonplaceholder/post/Post.java
new file mode 100644
index 0000000..bc4d7b0
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/post/Post.java
@@ -0,0 +1,112 @@
+package com.hoelee.jsonplaceholder.post;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.Table;
+import jakarta.persistence.Version;
+import java.time.Instant;
+import java.util.UUID;
+import org.hibernate.annotations.UuidGenerator;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+/**
+ * @version v1, 2024-10-23 10:15:00PM
+ * @author hoelee
+ * Learning note: the entity owns persistence concerns, while request validation remains at the API boundary.
+ */
+@Entity
+@Table(name = "posts", indexes = @Index(name = "idx_posts_title", columnList = "title"))
+@EntityListeners(AuditingEntityListener.class)
+public class Post {
+
+ @Id
+ @GeneratedValue
+ @UuidGenerator
+ private UUID id;
+
+ @Column(name = "source_post_id", unique = true)
+ private Long sourcePostId;
+
+ @Column(nullable = false)
+ private long authorId;
+
+ @Column(nullable = false, length = 160)
+ private String title;
+
+ @Column(nullable = false, length = 10_000)
+ private String body;
+
+ @Version
+ private long version;
+
+ @CreatedDate
+ @Column(nullable = false, updatable = false)
+ private Instant createdAt;
+
+ @LastModifiedDate
+ @Column(nullable = false)
+ private Instant updatedAt;
+
+ protected Post() {
+ }
+
+ private Post(Long sourcePostId, long authorId, String title, String body) {
+ this.sourcePostId = sourcePostId;
+ this.authorId = authorId;
+ this.title = title;
+ this.body = body;
+ }
+
+ public static Post create(long authorId, String title, String body) {
+ return new Post(null, authorId, title, body);
+ }
+
+ public static Post imported(long sourcePostId, long authorId, String title, String body) {
+ return new Post(sourcePostId, authorId, title, body);
+ }
+
+ public void update(String title, String body) {
+ this.title = title;
+ this.body = body;
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public long getAuthorId() {
+ return authorId;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public long getVersion() {
+ return version;
+ }
+
+ public Instant getCreatedAt() {
+ return createdAt;
+ }
+
+ public Instant getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public Long getSourcePostId() {
+ return sourcePostId;
+ }
+}
+
+//~ v1, 2024-10-23 10:15:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/post/PostController.java b/src/main/java/com/hoelee/jsonplaceholder/post/PostController.java
new file mode 100644
index 0000000..00bff54
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/post/PostController.java
@@ -0,0 +1,75 @@
+package com.hoelee.jsonplaceholder.post;
+
+import jakarta.validation.Valid;
+import java.net.URI;
+import java.util.UUID;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+
+/**
+ * @version v1, 2024-10-27 09:35:00PM
+ * @author hoelee
+ * Learning note: the controller is deliberately thin: it translates HTTP, delegates, and returns standard status codes.
+ */
+@RestController
+@RequestMapping("/api/posts")
+public class PostController {
+
+ private final PostService postService;
+
+ public PostController(PostService postService) {
+ this.postService = postService;
+ }
+
+ @GetMapping
+ public PageResponse findPosts(
+ @RequestParam(required = false) String title,
+ @PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {
+ return postService.findPosts(title, pageable);
+ }
+
+ @GetMapping("/{postId}")
+ public PostResponse findPost(@PathVariable UUID postId) {
+ return postService.findPost(postId);
+ }
+
+ @PostMapping
+ public ResponseEntity createPost(@Valid @RequestBody CreatePostRequest request) {
+ PostResponse response = postService.createPost(request);
+ URI location = ServletUriComponentsBuilder.fromCurrentRequest()
+ .path("/{postId}")
+ .buildAndExpand(response.id())
+ .toUri();
+ return ResponseEntity.created(location).body(response);
+ }
+
+ @PutMapping("/{postId}")
+ public PostResponse updatePost(@PathVariable UUID postId, @Valid @RequestBody UpdatePostRequest request) {
+ return postService.updatePost(postId, request);
+ }
+
+ @DeleteMapping("/{postId}")
+ public ResponseEntity deletePost(@PathVariable UUID postId) {
+ postService.deletePost(postId);
+ return ResponseEntity.noContent().build();
+ }
+
+ @PostMapping("/import/jsonplaceholder")
+ public ResponseEntity importFromJsonPlaceholder() {
+ return ResponseEntity.status(HttpStatus.CREATED).body(postService.importFromJsonPlaceholder());
+ }
+}
+
+//~ v1, 2024-10-27 09:35:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/post/PostRepository.java b/src/main/java/com/hoelee/jsonplaceholder/post/PostRepository.java
new file mode 100644
index 0000000..51a5688
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/post/PostRepository.java
@@ -0,0 +1,22 @@
+package com.hoelee.jsonplaceholder.post;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+/**
+ * @version v1, 2024-10-24 08:20:00PM
+ * @author hoelee
+ * Learning note: Spring Data derives focused queries from method names and keeps data access out of controllers.
+ */
+public interface PostRepository extends JpaRepository {
+
+ Page findByTitleContainingIgnoreCase(String title, Pageable pageable);
+
+ List findAllBySourcePostIdIn(Collection sourcePostIds);
+}
+
+//~ v1, 2024-10-24 08:20:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/post/PostResponse.java b/src/main/java/com/hoelee/jsonplaceholder/post/PostResponse.java
new file mode 100644
index 0000000..c4ee1c3
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/post/PostResponse.java
@@ -0,0 +1,32 @@
+package com.hoelee.jsonplaceholder.post;
+
+import java.time.Instant;
+import java.util.UUID;
+
+/**
+ * @version v1, 2024-10-25 08:25:00PM
+ * @author hoelee
+ * Learning note: response DTOs stop database implementation details from becoming a permanent API contract.
+ */
+public record PostResponse(
+ UUID id,
+ long authorId,
+ String title,
+ String body,
+ long version,
+ Instant createdAt,
+ Instant updatedAt) {
+
+ public static PostResponse from(Post post) {
+ return new PostResponse(
+ post.getId(),
+ post.getAuthorId(),
+ post.getTitle(),
+ post.getBody(),
+ post.getVersion(),
+ post.getCreatedAt(),
+ post.getUpdatedAt());
+ }
+}
+
+//~ v1, 2024-10-25 08:25:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/post/PostService.java b/src/main/java/com/hoelee/jsonplaceholder/post/PostService.java
new file mode 100644
index 0000000..6598446
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/post/PostService.java
@@ -0,0 +1,121 @@
+package com.hoelee.jsonplaceholder.post;
+
+import com.hoelee.jsonplaceholder.config.ApiProperties;
+import com.hoelee.jsonplaceholder.integration.JsonPlaceholderClient;
+import com.hoelee.jsonplaceholder.integration.JsonPlaceholderPost;
+import com.hoelee.jsonplaceholder.support.PostNotFoundException;
+import com.hoelee.jsonplaceholder.support.PostVersionConflictException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+/**
+ * @version v1, 2024-10-27 08:35:00PM
+ * @author hoelee
+ * Learning note: this service owns business transactions, cache invalidation, and integration orchestration.
+ */
+@Service
+public class PostService {
+
+ private final PostRepository postRepository;
+ private final JsonPlaceholderClient jsonPlaceholderClient;
+ private final ApiProperties apiProperties;
+
+ public PostService(
+ PostRepository postRepository,
+ JsonPlaceholderClient jsonPlaceholderClient,
+ ApiProperties apiProperties) {
+ this.postRepository = postRepository;
+ this.jsonPlaceholderClient = jsonPlaceholderClient;
+ this.apiProperties = apiProperties;
+ }
+
+ @Transactional(readOnly = true)
+ public PageResponse findPosts(String title, Pageable pageable) {
+ Page posts = StringUtils.hasText(title)
+ ? postRepository.findByTitleContainingIgnoreCase(title.trim(), pageable)
+ : postRepository.findAll(pageable);
+ return PageResponse.from(posts, PostResponse::from);
+ }
+
+ @Cacheable(cacheNames = "posts", key = "#postId")
+ @Transactional(readOnly = true)
+ public PostResponse findPost(UUID postId) {
+ return postRepository.findById(postId)
+ .map(PostResponse::from)
+ .orElseThrow(() -> new PostNotFoundException(postId));
+ }
+
+ @Transactional
+ public PostResponse createPost(CreatePostRequest request) {
+ Post post = Post.create(request.authorId(), request.title().trim(), request.body().trim());
+ return PostResponse.from(postRepository.saveAndFlush(post));
+ }
+
+ @CachePut(cacheNames = "posts", key = "#postId")
+ @Transactional
+ public PostResponse updatePost(UUID postId, UpdatePostRequest request) {
+ Post post = postRepository.findById(postId)
+ .orElseThrow(() -> new PostNotFoundException(postId));
+ if (post.getVersion() != request.version()) {
+ throw new PostVersionConflictException(postId);
+ }
+ post.update(request.title().trim(), request.body().trim());
+ return PostResponse.from(postRepository.saveAndFlush(post));
+ }
+
+ @CacheEvict(cacheNames = "posts", key = "#postId")
+ @Transactional
+ public void deletePost(UUID postId) {
+ Post post = postRepository.findById(postId)
+ .orElseThrow(() -> new PostNotFoundException(postId));
+ postRepository.delete(post);
+ }
+
+ @CacheEvict(cacheNames = "posts", allEntries = true)
+ @Transactional
+ public ImportResult importFromJsonPlaceholder() {
+ List received = jsonPlaceholderClient.fetchPosts(apiProperties.importLimit());
+ List candidates = received.stream()
+ .filter(this::isImportable)
+ .toList();
+ Set knownSourceIds = postRepository.findAllBySourcePostIdIn(
+ candidates.stream().map(JsonPlaceholderPost::id).toList())
+ .stream()
+ .map(Post::getSourcePostId)
+ .collect(java.util.stream.Collectors.toSet());
+ Set seenSourceIds = new HashSet<>();
+ List newPosts = candidates.stream()
+ .filter(remotePost -> !knownSourceIds.contains(remotePost.id()))
+ .filter(remotePost -> seenSourceIds.add(remotePost.id()))
+ .map(remotePost -> Post.imported(
+ remotePost.id(),
+ remotePost.userId(),
+ remotePost.title().trim(),
+ remotePost.body().trim()))
+ .toList();
+ postRepository.saveAllAndFlush(newPosts);
+ return new ImportResult(received.size(), newPosts.size(), received.size() - newPosts.size());
+ }
+
+ private boolean isImportable(JsonPlaceholderPost post) {
+ return post.id() != null
+ && post.userId() != null
+ && post.userId() > 0
+ && StringUtils.hasText(post.title())
+ && post.title().length() <= 160
+ && StringUtils.hasText(post.body())
+ && post.body().length() <= 10_000;
+ }
+}
+
+//~ v1, 2024-10-27 08:35:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/post/UpdatePostRequest.java b/src/main/java/com/hoelee/jsonplaceholder/post/UpdatePostRequest.java
new file mode 100644
index 0000000..2c8c351
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/post/UpdatePostRequest.java
@@ -0,0 +1,19 @@
+package com.hoelee.jsonplaceholder.post;
+
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+/**
+ * @version v1, 2024-10-24 10:20:00PM
+ * @author hoelee
+ * Learning note: accepting a version makes concurrent edits explicit instead of silently overwriting data.
+ */
+public record UpdatePostRequest(
+ @NotBlank @Size(max = 160) String title,
+ @NotBlank @Size(max = 10_000) String body,
+ @NotNull @Min(0) Long version) {
+}
+
+//~ v1, 2024-10-24 10:20:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/support/ApiExceptionHandler.java b/src/main/java/com/hoelee/jsonplaceholder/support/ApiExceptionHandler.java
new file mode 100644
index 0000000..c66e6dd
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/support/ApiExceptionHandler.java
@@ -0,0 +1,64 @@
+package com.hoelee.jsonplaceholder.support;
+
+import java.net.URI;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ProblemDetail;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+/**
+ * @version v1, 2024-10-27 10:35:00PM
+ * @author hoelee
+ * Learning note: one error boundary makes RFC 9457-style problem responses consistent across every endpoint.
+ */
+@RestControllerAdvice
+public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
+
+ @ExceptionHandler(PostNotFoundException.class)
+ ProblemDetail handleNotFound(PostNotFoundException exception) {
+ return problem(HttpStatus.NOT_FOUND, "POST_NOT_FOUND", exception.getMessage());
+ }
+
+ @ExceptionHandler(PostVersionConflictException.class)
+ ProblemDetail handleConflict(PostVersionConflictException exception) {
+ return problem(HttpStatus.CONFLICT, "POST_VERSION_CONFLICT", exception.getMessage());
+ }
+
+ @ExceptionHandler({UpstreamServiceException.class, RestClientException.class})
+ ProblemDetail handleUpstreamFailure(RuntimeException exception) {
+ return problem(HttpStatus.SERVICE_UNAVAILABLE, "UPSTREAM_UNAVAILABLE",
+ "The post import service is temporarily unavailable");
+ }
+
+ @Override
+ protected ResponseEntity handleMethodArgumentNotValid(
+ MethodArgumentNotValidException exception,
+ org.springframework.http.HttpHeaders headers,
+ org.springframework.http.HttpStatusCode status,
+ WebRequest request) {
+ Map errors = new LinkedHashMap<>();
+ for (FieldError fieldError : exception.getBindingResult().getFieldErrors()) {
+ errors.putIfAbsent(fieldError.getField(), fieldError.getDefaultMessage());
+ }
+ ProblemDetail problem = problem(HttpStatus.BAD_REQUEST, "VALIDATION_FAILED", "One or more fields are invalid");
+ problem.setProperty("errors", errors);
+ return ResponseEntity.badRequest().body(problem);
+ }
+
+ private ProblemDetail problem(HttpStatus status, String code, String detail) {
+ ProblemDetail problem = ProblemDetail.forStatusAndDetail(status, detail);
+ problem.setType(URI.create("https://github.com/hoelee/springboot-jsonplaceholder-demo/problems/" + code));
+ problem.setProperty("code", code);
+ return problem;
+ }
+}
+
+//~ v1, 2024-10-27 10:35:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/support/PostNotFoundException.java b/src/main/java/com/hoelee/jsonplaceholder/support/PostNotFoundException.java
new file mode 100644
index 0000000..584ca23
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/support/PostNotFoundException.java
@@ -0,0 +1,17 @@
+package com.hoelee.jsonplaceholder.support;
+
+import java.util.UUID;
+
+/**
+ * @version v1, 2024-10-26 08:30:00PM
+ * @author hoelee
+ * Learning note: domain-specific exceptions let the HTTP adapter provide precise client feedback.
+ */
+public class PostNotFoundException extends RuntimeException {
+
+ public PostNotFoundException(UUID postId) {
+ super("Post %s was not found".formatted(postId));
+ }
+}
+
+//~ v1, 2024-10-26 08:30:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/support/PostVersionConflictException.java b/src/main/java/com/hoelee/jsonplaceholder/support/PostVersionConflictException.java
new file mode 100644
index 0000000..fbba20a
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/support/PostVersionConflictException.java
@@ -0,0 +1,17 @@
+package com.hoelee.jsonplaceholder.support;
+
+import java.util.UUID;
+
+/**
+ * @version v1, 2024-10-26 09:30:00PM
+ * @author hoelee
+ * Learning note: optimistic locking gives API clients a clear retry signal when data changed concurrently.
+ */
+public class PostVersionConflictException extends RuntimeException {
+
+ public PostVersionConflictException(UUID postId) {
+ super("Post %s has changed; fetch it again before retrying".formatted(postId));
+ }
+}
+
+//~ v1, 2024-10-26 09:30:00PM - Last edited by hoelee
diff --git a/src/main/java/com/hoelee/jsonplaceholder/support/UpstreamServiceException.java b/src/main/java/com/hoelee/jsonplaceholder/support/UpstreamServiceException.java
new file mode 100644
index 0000000..b574d1f
--- /dev/null
+++ b/src/main/java/com/hoelee/jsonplaceholder/support/UpstreamServiceException.java
@@ -0,0 +1,15 @@
+package com.hoelee.jsonplaceholder.support;
+
+/**
+ * @version v1, 2024-10-26 10:30:00PM
+ * @author hoelee
+ * Learning note: wrapping remote client errors prevents third-party exception details leaking through the API.
+ */
+public class UpstreamServiceException extends RuntimeException {
+
+ public UpstreamServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
+
+//~ v1, 2024-10-26 10:30:00PM - Last edited by hoelee
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
deleted file mode 100644
index a755768..0000000
--- a/src/main/resources/application.properties
+++ /dev/null
@@ -1,10 +0,0 @@
-spring.server.port=10080
-
-
-
-
-
-
-
-
-
diff --git a/src/main/resources/rebel.xml b/src/main/resources/rebel.xml
deleted file mode 100644
index dfe6fa3..0000000
--- a/src/main/resources/rebel.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/main/resources/templates/testing.html b/src/main/resources/templates/testing.html
deleted file mode 100644
index c627c24..0000000
--- a/src/main/resources/templates/testing.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
- TODO supply a title
-
-
-
-
- TODO write content
-
-
diff --git a/src/main/webapp/META-INF/context.xml b/src/main/webapp/META-INF/context.xml
deleted file mode 100644
index fd2fac1..0000000
--- a/src/main/webapp/META-INF/context.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/src/main/webapp/WEB-INF/thymeleaf/testing.html b/src/main/webapp/WEB-INF/thymeleaf/testing.html
deleted file mode 100644
index c627c24..0000000
--- a/src/main/webapp/WEB-INF/thymeleaf/testing.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
- TODO supply a title
-
-
-
-
- TODO write content
-
-
diff --git a/src/test/java/com/hoelee/demo/demo/DemoApplicationTests.java b/src/test/java/com/hoelee/demo/demo/DemoApplicationTests.java
deleted file mode 100644
index 845fdb4..0000000
--- a/src/test/java/com/hoelee/demo/demo/DemoApplicationTests.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.hoelee.demo.demo;
-
-import org.junit.jupiter.api.Test;
-
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class DemoApplicationTests {
-
- @Test
- void contextLoads() {}
-}
-
-//~ v2, 2020-09-30 12:23:32AM - Last edited by hoelee