Add secure cached post API and remote import
This commit is contained in:
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project-shared-configuration>
|
||||
<!--
|
||||
This file contains additional configuration written by modules in the NetBeans IDE.
|
||||
The configuration is intended to be shared among all the users of project and
|
||||
therefore it is assumed to be part of version control checkout.
|
||||
Without this configuration present, some functionality in the IDE may be limited or fail altogether.
|
||||
-->
|
||||
<properties xmlns="http://www.netbeans.org/ns/maven-properties-data/1">
|
||||
<!--
|
||||
Properties that influence various parts of the IDE, especially code formatting and the like.
|
||||
You can copy and paste the single properties, into the pom.xml file and the IDE will pick them up.
|
||||
That way multiple projects can share the same settings (useful for formatting rules for example).
|
||||
Any value defined here will override the pom.xml file value but is only applicable to the current project.
|
||||
-->
|
||||
<org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_deploy_2e_server>Tomcat</org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_deploy_2e_server>
|
||||
<org-netbeans-modules-css-prep.less_2e_mappings>/less:/css</org-netbeans-modules-css-prep.less_2e_mappings>
|
||||
<org-netbeans-modules-css-prep.less_2e_enabled>false</org-netbeans-modules-css-prep.less_2e_enabled>
|
||||
<org-netbeans-modules-css-prep.sass_2e_enabled>false</org-netbeans-modules-css-prep.sass_2e_enabled>
|
||||
<org-netbeans-modules-css-prep.sass_2e_compiler_2e_options/>
|
||||
<org-netbeans-modules-css-prep.less_2e_compiler_2e_options/>
|
||||
<org-netbeans-modules-css-prep.sass_2e_mappings>/scss:/css</org-netbeans-modules-css-prep.sass_2e_mappings>
|
||||
<org-netbeans-modules-maven-j2ee.netbeans_2e_deploy_2e_on_2e_save>false</org-netbeans-modules-maven-j2ee.netbeans_2e_deploy_2e_on_2e_save>
|
||||
<org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_j2eeVersion>1.7-web</org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_j2eeVersion>
|
||||
</properties>
|
||||
</project-shared-configuration>
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>Class desc:</p>
|
||||
*
|
||||
*
|
||||
* @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
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
*
|
||||
* @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
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
@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
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>Class desc:</p>
|
||||
*
|
||||
*
|
||||
* @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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>Class desc:</p>
|
||||
*
|
||||
* @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<Post> postList = internetHelper.readAllPosts();
|
||||
List<Comment> commentList = internetHelper.readAllComments();
|
||||
|
||||
// Distribute respective Comment into each Post
|
||||
for(int a = 0; a < postList.size(); a++) {
|
||||
Post post = postList.get(a);
|
||||
List<Comment> 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<Post>() {
|
||||
|
||||
@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
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
*
|
||||
* @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<Comment> 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<Comment> commentListPostId = new LinkedList<>();
|
||||
List<Comment> commnetListCommentId = new LinkedList<>();
|
||||
List<Comment> commentListName = new LinkedList<>();
|
||||
List<Comment> commentListEmail = new LinkedList<>();
|
||||
List<Comment> commentListBody = new LinkedList<>();
|
||||
List<Comment> 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<Comment> 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<Comment> commentTotal = new LinkedList<>();
|
||||
|
||||
if (!((postId == null) || postId.isEmpty())) {
|
||||
List<Comment> 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<Comment> 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<Comment> 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<Comment> 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<Comment> 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<Comment> 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<Comment> 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<Comment> 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<Comment> 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<Comment> 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
|
||||
@@ -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<Comment> commentList = internetHelper.readAllComments();
|
||||
List<Post> postList = new LinkedList<>();
|
||||
Post post1 = new Post();
|
||||
List<Comment> commentList1 = new LinkedList<>();
|
||||
|
||||
commentList1.add(new Comment());
|
||||
commentList1.add(new Comment());
|
||||
commentList1.add(new Comment());
|
||||
post1.setCommentList(commentList1);
|
||||
|
||||
Post post2 = new Post();
|
||||
List<Comment> commentList2 = new LinkedList<>();
|
||||
|
||||
commentList2.add(new Comment());
|
||||
commentList2.add(new Comment());
|
||||
post2.setCommentList(commentList2);
|
||||
|
||||
Post post3 = new Post();
|
||||
List<Comment> commentList3 = new LinkedList<>();
|
||||
|
||||
commentList3.add(new Comment());
|
||||
post3.setCommentList(commentList3);
|
||||
postList.add(post1);
|
||||
postList.add(post2);
|
||||
postList.add(post3);
|
||||
Collections.sort(postList,
|
||||
new Comparator<Post>() {
|
||||
|
||||
@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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>Class desc:</p>
|
||||
*
|
||||
*
|
||||
* @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
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>Class desc:</p>
|
||||
*
|
||||
*
|
||||
* @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<Comment> commentList = new LinkedList<>();
|
||||
private int userId;
|
||||
private int id;
|
||||
private String title;
|
||||
private String body;
|
||||
|
||||
/** hoelee v2 2020-10-02 11:47:11AM
|
||||
* <p>Constructor desc:</p>
|
||||
*
|
||||
*/
|
||||
public Post() {}
|
||||
|
||||
/** hoelee v2 2020-10-02 11:47:11AM
|
||||
* <p>Constructor desc:</p>
|
||||
*
|
||||
*
|
||||
* @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
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public int getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
/** hoelee v2 2020-10-02 11:47:11AM
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
*
|
||||
* @param userId
|
||||
*/
|
||||
public void setUserId(int userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
/** hoelee v2 2020-10-02 11:47:11AM
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/** hoelee v2 2020-10-02 11:47:11AM
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/** hoelee v2 2020-10-02 11:47:11AM
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
/** hoelee v2 2020-10-02 11:47:11AM
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
*
|
||||
* @param title
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/** hoelee v2 2020-10-02 11:47:11AM
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
/** hoelee v2 2020-10-02 11:47:11AM
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
*
|
||||
* @param body
|
||||
*/
|
||||
public void setBody(String body) {
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
/** hoelee v2 2020-10-02 11:47:11AM
|
||||
* <p>Method desc:</p>
|
||||
* All the comments for this post
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<Comment> getCommentList() {
|
||||
return commentList;
|
||||
}
|
||||
|
||||
/** hoelee v2 2020-10-02 11:47:11AM
|
||||
* <p>Method desc:</p>
|
||||
* All the comments for this post
|
||||
*
|
||||
* @param commentList
|
||||
*/
|
||||
public void setCommentList(List<Comment> commentList) {
|
||||
this.commentList = commentList;
|
||||
}
|
||||
|
||||
/** hoelee v2 2020-10-02 11:47:11AM
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
*
|
||||
* @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
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
* @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
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.hoelee.demo.demo.exception;
|
||||
|
||||
/**
|
||||
* <p>Class desc:</p>
|
||||
* 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
|
||||
* <p>Constructor desc:</p>
|
||||
*
|
||||
*
|
||||
* @param message
|
||||
*/
|
||||
public ExceptionJSONConversion(String message) {
|
||||
super(message);
|
||||
|
||||
this.modelClass = modelClass;
|
||||
}
|
||||
|
||||
/** hoelee v2 2020-10-01 06:39:32PM
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Class getModelClass() {
|
||||
return modelClass;
|
||||
}
|
||||
}
|
||||
|
||||
//~ v2, 2020-10-01 06:39:32PM - Last edited by hoelee
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>Class desc:</p>
|
||||
*
|
||||
*
|
||||
* @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
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
*
|
||||
* @param request
|
||||
*/
|
||||
@Autowired
|
||||
public void setRequest(HttpServletRequest request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
/** hoelee v2 2020-10-01 06:10:46PM
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<Comment> readAllComments() {
|
||||
String epReadComments = "https://jsonplaceholder.typicode.com/comments";
|
||||
String response = synchronizeRequestGetMethod(epReadComments);
|
||||
|
||||
try {
|
||||
JSONArray ja = new JSONArray(response);
|
||||
List<Comment> 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
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<Post> readAllPosts() {
|
||||
String epReadPosts = "https://jsonplaceholder.typicode.com/posts";
|
||||
String response = synchronizeRequestGetMethod(epReadPosts);
|
||||
|
||||
try {
|
||||
JSONArray ja = new JSONArray(response);
|
||||
List<Post> 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
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
*
|
||||
* @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
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
*
|
||||
* @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
|
||||
* <p>Method desc:</p>
|
||||
*
|
||||
*
|
||||
* @param url
|
||||
* @return
|
||||
*/
|
||||
private String synchronizeRequestGetMethod(String url) {
|
||||
CookieJar cookieJar = new CookieJar() {
|
||||
|
||||
@Override
|
||||
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
|
||||
|
||||
// Save Cookies
|
||||
String urlString = url.toString();
|
||||
|
||||
for(Cookie cookie : cookies) {
|
||||
String cookieString = cookie.toString();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public List<Cookie> loadForRequest(HttpUrl url) {
|
||||
|
||||
// Load new cookies
|
||||
ArrayList<Cookie> 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
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Class desc:</p>
|
||||
*
|
||||
* @version v2, 2020-10-02 02:48:54PM
|
||||
* @author hoelee
|
||||
*/
|
||||
public class ListHelper {
|
||||
|
||||
/**
|
||||
* hoelee v2 2020-10-02 02:48:54PM
|
||||
* <p>
|
||||
* Method desc:</p>
|
||||
* 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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<JsonPlaceholderPost> fetchPosts(int limit) {
|
||||
try {
|
||||
List<JsonPlaceholderPost> 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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<T>(
|
||||
List<T> content,
|
||||
int page,
|
||||
int size,
|
||||
long totalElements,
|
||||
int totalPages) {
|
||||
|
||||
public static <S, T> PageResponse<T> from(Page<S> page, Function<S, T> 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
|
||||
112
src/main/java/com/hoelee/jsonplaceholder/post/Post.java
Normal file
112
src/main/java/com/hoelee/jsonplaceholder/post/Post.java
Normal file
@@ -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
|
||||
@@ -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<PostResponse> 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<PostResponse> 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<Void> deletePost(@PathVariable UUID postId) {
|
||||
postService.deletePost(postId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping("/import/jsonplaceholder")
|
||||
public ResponseEntity<ImportResult> importFromJsonPlaceholder() {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(postService.importFromJsonPlaceholder());
|
||||
}
|
||||
}
|
||||
|
||||
//~ v1, 2024-10-27 09:35:00PM - Last edited by hoelee
|
||||
@@ -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<Post, UUID> {
|
||||
|
||||
Page<Post> findByTitleContainingIgnoreCase(String title, Pageable pageable);
|
||||
|
||||
List<Post> findAllBySourcePostIdIn(Collection<Long> sourcePostIds);
|
||||
}
|
||||
|
||||
//~ v1, 2024-10-24 08:20:00PM - Last edited by hoelee
|
||||
@@ -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
|
||||
121
src/main/java/com/hoelee/jsonplaceholder/post/PostService.java
Normal file
121
src/main/java/com/hoelee/jsonplaceholder/post/PostService.java
Normal file
@@ -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<PostResponse> findPosts(String title, Pageable pageable) {
|
||||
Page<Post> 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<JsonPlaceholderPost> received = jsonPlaceholderClient.fetchPosts(apiProperties.importLimit());
|
||||
List<JsonPlaceholderPost> candidates = received.stream()
|
||||
.filter(this::isImportable)
|
||||
.toList();
|
||||
Set<Long> knownSourceIds = postRepository.findAllBySourcePostIdIn(
|
||||
candidates.stream().map(JsonPlaceholderPost::id).toList())
|
||||
.stream()
|
||||
.map(Post::getSourcePostId)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
Set<Long> seenSourceIds = new HashSet<>();
|
||||
List<Post> 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
|
||||
@@ -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
|
||||
@@ -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<Object> handleMethodArgumentNotValid(
|
||||
MethodArgumentNotValidException exception,
|
||||
org.springframework.http.HttpHeaders headers,
|
||||
org.springframework.http.HttpStatusCode status,
|
||||
WebRequest request) {
|
||||
Map<String, String> 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,10 +0,0 @@
|
||||
spring.server.port=10080
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
This is the JRebel configuration file. It maps the running application to your IDE workspace, enabling JRebel reloading for this project.
|
||||
Refer to https://manuals.jrebel.com/jrebel/standalone/config.html for more information.
|
||||
-->
|
||||
<application generated-by="netbeans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.zeroturnaround.com" xsi:schemaLocation="http://www.zeroturnaround.com http://update.zeroturnaround.com/jrebel/rebel-2_1.xsd">
|
||||
|
||||
<classpath>
|
||||
<dir name="C:\Users\hoelee\Documents\NetBeansProjects\demo\target\classes">
|
||||
</dir>
|
||||
</classpath>
|
||||
|
||||
<web>
|
||||
<link target="/">
|
||||
<dir name="C:\Users\hoelee\Documents\NetBeansProjects\demo\src\main\webapp">
|
||||
</dir>
|
||||
</link>
|
||||
</web>
|
||||
|
||||
</application>
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
To change this license header, choose License Headers in Project Properties.
|
||||
To change this template file, choose Tools | Templates
|
||||
and open the template in the editor.
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>TODO supply a title</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<div>TODO write content</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Context path=""/>
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
To change this license header, choose License Headers in Project Properties.
|
||||
To change this template file, choose Tools | Templates
|
||||
and open the template in the editor.
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>TODO supply a title</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<div>TODO write content</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user