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