Phần Post đã thiết kế xong chỉ cần sửa lại tempalte nữa là được
This commit is contained in:
@@ -14,13 +14,12 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.h2console.autoconfigure.H2ConsoleAutoConfiguration;
|
||||
import org.springframework.boot.liquibase.autoconfigure.LiquibaseProperties;
|
||||
import org.springframework.core.env.Environment;
|
||||
import tech.jhipster.config.DefaultProfileUtil;
|
||||
import tech.jhipster.config.JHipsterConstants;
|
||||
|
||||
@SpringBootApplication(exclude = { H2ConsoleAutoConfiguration.class })
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties({ LiquibaseProperties.class, ApplicationProperties.class })
|
||||
public class SisvietnamvnApp {
|
||||
|
||||
|
||||
-32
@@ -5,7 +5,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.h2console.autoconfigure.H2ConsoleProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
@@ -14,13 +13,11 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
import tech.jhipster.config.JHipsterConstants;
|
||||
import tech.jhipster.config.h2.H2ConfigurationHelper;
|
||||
|
||||
@Configuration
|
||||
@EnableJpaRepositories({ "com.sisvietnamvn.web.repository" })
|
||||
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
|
||||
@EnableTransactionManagement
|
||||
@EnableConfigurationProperties(H2ConsoleProperties.class)
|
||||
public class DatabaseConfiguration {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DatabaseConfiguration.class);
|
||||
@@ -30,33 +27,4 @@ public class DatabaseConfiguration {
|
||||
public DatabaseConfiguration(Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the TCP port for the H2 database, so it is available remotely.
|
||||
*
|
||||
* @return the H2 database TCP server.
|
||||
* @throws SQLException if the server failed to start.
|
||||
*/
|
||||
@Bean(initMethod = "start", destroyMethod = "stop")
|
||||
@Profile(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT)
|
||||
@ConditionalOnProperty(prefix = "spring.h2.console", name = "enabled", havingValue = "true")
|
||||
public Object h2TCPServer() throws SQLException {
|
||||
String port = getValidPortForH2();
|
||||
LOG.debug("H2 database is available on port {}", port);
|
||||
return H2ConfigurationHelper.createServer(port);
|
||||
}
|
||||
|
||||
private String getValidPortForH2() {
|
||||
var port = Integer.parseInt(env.getProperty("server.port"));
|
||||
if (port < 10000) {
|
||||
port = 10000 + port;
|
||||
} else {
|
||||
if (port < 63536) {
|
||||
port = port + 2000;
|
||||
} else {
|
||||
port = port - 2000;
|
||||
}
|
||||
}
|
||||
return String.valueOf(port);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ public class SecurityConfiguration {
|
||||
authz
|
||||
.requestMatchers(HttpMethod.GET, "/", "/about", "/flex-finish", "/tin-tuc", "/tin-tuc/**", "/lien-he",
|
||||
"/manage/**", "/css/**", "/images/**", "/js/**", "/UMass*/**", "/Undergraduate*/**",
|
||||
"/favicon.ico", "/favicons/**", "/flex-finish/**", "/contact-us/**", "/uploads/**", "/api/manage/snippets/**", "/page/**",
|
||||
"/favicon.ico", "/favicons/**", "/flex-finish/**", "/contact-us/**", "/uploads/**", "/api/manage/snippets/**", "/page/**", "/news/article/**", "/post/**", "/error",
|
||||
"/about-us", "/specialty", "/doctor", "/service", "/health-library", "/news-and-events", "/patient-support", "/medical-expert", "/umcers", "/bidding", "/contact-us")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/manage/**", "/api/manage/media/upload").permitAll()
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.sisvietnamvn.web.controller;
|
||||
|
||||
import com.sisvietnamvn.web.domain.PageStatus;
|
||||
import com.sisvietnamvn.web.domain.Post;
|
||||
import com.sisvietnamvn.web.repository.PostRepository;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/post")
|
||||
public class PostController {
|
||||
|
||||
private final PostRepository postRepository;
|
||||
|
||||
public PostController(PostRepository postRepository) {
|
||||
this.postRepository = postRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/{slug}")
|
||||
public String viewPost(@PathVariable String slug, Model model) {
|
||||
Post post = postRepository.findBySlug(slug)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
|
||||
// Only allow viewing published posts (unless admin, but for now just check published)
|
||||
if (post.getStatus() != PageStatus.PUBLISHED) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
model.addAttribute("post", post);
|
||||
|
||||
// Safely format the Instant to avoid Thymeleaf parsing errors
|
||||
if (post.getCreatedDate() != null) {
|
||||
String formattedDate = java.time.format.DateTimeFormatter.ofPattern("MMMM dd, yyyy")
|
||||
.withZone(java.time.ZoneId.systemDefault())
|
||||
.format(post.getCreatedDate());
|
||||
model.addAttribute("formattedDate", formattedDate);
|
||||
}
|
||||
|
||||
switch (post.getLayout()) {
|
||||
case SIDEBAR:
|
||||
return "posts/sidebar";
|
||||
case FULL_WIDTH:
|
||||
return "posts/full-width";
|
||||
case STANDARD:
|
||||
default:
|
||||
return "posts/standard";
|
||||
}
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package com.sisvietnamvn.web.controller.manage;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Category;
|
||||
import com.sisvietnamvn.web.hook.HookManager;
|
||||
import com.sisvietnamvn.web.service.CategoryService;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
/**
|
||||
* Controller for managing post Categories in the admin panel.
|
||||
* Uses a WordPress-style split layout: form on the left, table on the right.
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/manage/posts/categories")
|
||||
public class ManageCategoryController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ManageCategoryController.class);
|
||||
|
||||
private final CategoryService categoryService;
|
||||
private final HookManager hookManager;
|
||||
|
||||
public ManageCategoryController(CategoryService categoryService, HookManager hookManager) {
|
||||
this.categoryService = categoryService;
|
||||
this.hookManager = hookManager;
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public void adminInit() {
|
||||
hookManager.doAction("admin_init");
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /manage/posts/categories — List all categories with inline create form.
|
||||
*/
|
||||
@GetMapping
|
||||
public String listCategories(Model model) {
|
||||
LOG.debug("Request to list all categories");
|
||||
populateModel(model, new Category(), true);
|
||||
return "manage/posts/categories";
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /manage/posts/categories — Save a new category.
|
||||
*/
|
||||
@PostMapping
|
||||
public String createCategory(@Valid @ModelAttribute("category") Category category,
|
||||
BindingResult bindingResult,
|
||||
@RequestParam(value = "parentId", required = false) Long parentId,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to create Category : {}", category);
|
||||
if (bindingResult.hasErrors()) {
|
||||
populateModel(model, category, true);
|
||||
return "manage/posts/categories";
|
||||
}
|
||||
if (parentId != null) {
|
||||
categoryService.findById(parentId).ifPresent(category::setParent);
|
||||
}
|
||||
categoryService.save(category);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Category created successfully!");
|
||||
return "redirect:/manage/posts/categories";
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /manage/posts/categories/{id}/edit — Show the edit form for a category.
|
||||
*/
|
||||
@GetMapping("/{id}/edit")
|
||||
public String showEditForm(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to show edit form for Category : {}", id);
|
||||
Optional<Category> categoryOptional = categoryService.findById(id);
|
||||
if (categoryOptional.isEmpty()) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Category not found.");
|
||||
return "redirect:/manage/posts/categories";
|
||||
}
|
||||
populateModel(model, categoryOptional.get(), false);
|
||||
return "manage/posts/categories";
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /manage/posts/categories/{id} — Update an existing category.
|
||||
*/
|
||||
@PostMapping("/{id}")
|
||||
public String updateCategory(@PathVariable Long id,
|
||||
@Valid @ModelAttribute("category") Category category,
|
||||
BindingResult bindingResult,
|
||||
@RequestParam(value = "parentId", required = false) Long parentId,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to update Category : {}", id);
|
||||
if (bindingResult.hasErrors()) {
|
||||
populateModel(model, category, false);
|
||||
return "manage/posts/categories";
|
||||
}
|
||||
category.setId(id);
|
||||
if (parentId != null && !parentId.equals(id)) {
|
||||
categoryService.findById(parentId).ifPresent(category::setParent);
|
||||
} else {
|
||||
category.setParent(null);
|
||||
}
|
||||
categoryService.save(category);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Category updated successfully!");
|
||||
return "redirect:/manage/posts/categories";
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /manage/posts/categories/{id}/delete — Delete a category.
|
||||
*/
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deleteCategory(@PathVariable Long id, RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to delete Category : {}", id);
|
||||
try {
|
||||
categoryService.delete(id);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Category deleted successfully!");
|
||||
} catch (IllegalStateException e) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", e.getMessage());
|
||||
}
|
||||
return "redirect:/manage/posts/categories";
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate common model attributes for the categories view.
|
||||
*/
|
||||
private void populateModel(Model model, Category category, boolean isNew) {
|
||||
model.addAttribute("category", category);
|
||||
model.addAttribute("isNew", isNew);
|
||||
model.addAttribute("allCategories", categoryService.findAll());
|
||||
// Build a map of category ID -> post count for the table
|
||||
java.util.Map<Long, Long> postCounts = new java.util.HashMap<>();
|
||||
for (Category c : categoryService.findAll()) {
|
||||
postCounts.put(c.getId(), categoryService.countPosts(c.getId()));
|
||||
}
|
||||
model.addAttribute("postCounts", postCounts);
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
package com.sisvietnamvn.web.controller.manage;
|
||||
|
||||
import com.sisvietnamvn.web.domain.PageStatus;
|
||||
import com.sisvietnamvn.web.domain.Post;
|
||||
import com.sisvietnamvn.web.domain.PostLayout;
|
||||
import com.sisvietnamvn.web.domain.Tag;
|
||||
import com.sisvietnamvn.web.hook.HookManager;
|
||||
import com.sisvietnamvn.web.service.CategoryService;
|
||||
import com.sisvietnamvn.web.service.PostService;
|
||||
import com.sisvietnamvn.web.service.TagService;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
/**
|
||||
* Controller for managing blog Posts in the admin panel.
|
||||
* Provides CRUD operations with category and tag management.
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/manage/posts")
|
||||
public class ManagePostController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ManagePostController.class);
|
||||
|
||||
private final PostService postService;
|
||||
private final CategoryService categoryService;
|
||||
private final TagService tagService;
|
||||
private final HookManager hookManager;
|
||||
|
||||
public ManagePostController(PostService postService,
|
||||
CategoryService categoryService,
|
||||
TagService tagService,
|
||||
HookManager hookManager) {
|
||||
this.postService = postService;
|
||||
this.categoryService = categoryService;
|
||||
this.tagService = tagService;
|
||||
this.hookManager = hookManager;
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public void adminInit() {
|
||||
hookManager.doAction("admin_init");
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /manage/posts — List all posts with optional filters.
|
||||
*/
|
||||
@GetMapping
|
||||
public String listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||
@RequestParam(value = "status", required = false) String statusStr,
|
||||
Model model) {
|
||||
LOG.debug("Request to list all posts (categoryId={}, status={})", categoryId, statusStr);
|
||||
|
||||
PageStatus status = null;
|
||||
if (statusStr != null && !statusStr.isBlank()) {
|
||||
try {
|
||||
status = PageStatus.valueOf(statusStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Invalid status filter: {}", statusStr);
|
||||
}
|
||||
}
|
||||
|
||||
model.addAttribute("posts", postService.findFiltered(categoryId, status));
|
||||
model.addAttribute("categories", categoryService.findAll());
|
||||
model.addAttribute("statuses", PageStatus.values());
|
||||
model.addAttribute("selectedCategoryId", categoryId);
|
||||
model.addAttribute("selectedStatus", statusStr);
|
||||
return "manage/posts/list";
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /manage/posts/new — Show the "Add New Post" form.
|
||||
*/
|
||||
@GetMapping("/new")
|
||||
public String showCreateForm(Model model) {
|
||||
LOG.debug("Request to show create post form");
|
||||
Post post = new Post();
|
||||
post.setStatus(PageStatus.DRAFT);
|
||||
populateFormModel(model, post, "", true);
|
||||
return "manage/posts/form";
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /manage/posts/create — Save a new post.
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
public String createPost(@Valid @ModelAttribute("post") Post post,
|
||||
BindingResult bindingResult,
|
||||
@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||
@RequestParam(value = "tagNames", required = false, defaultValue = "") String tagNames,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to create Post : {}", post);
|
||||
if (bindingResult.hasErrors()) {
|
||||
populateFormModel(model, post, tagNames, true);
|
||||
return "manage/posts/form";
|
||||
}
|
||||
if (categoryId != null) {
|
||||
categoryService.findById(categoryId).ifPresent(post::setCategory);
|
||||
}
|
||||
postService.saveWithTags(post, tagNames);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Post created successfully!");
|
||||
return "redirect:/manage/posts";
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /manage/posts/{id}/edit — Show the "Edit Post" form.
|
||||
*/
|
||||
@GetMapping("/{id}/edit")
|
||||
public String showEditForm(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to show edit form for Post : {}", id);
|
||||
Optional<Post> postOptional = postService.findById(id);
|
||||
if (postOptional.isEmpty()) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Post not found.");
|
||||
return "redirect:/manage/posts";
|
||||
}
|
||||
Post post = postOptional.get();
|
||||
String tagNames = post.getTags().stream()
|
||||
.map(Tag::getName)
|
||||
.collect(Collectors.joining(", "));
|
||||
populateFormModel(model, post, tagNames, false);
|
||||
return "manage/posts/form";
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /manage/posts/{id} — Update an existing post.
|
||||
*/
|
||||
@PostMapping("/{id}")
|
||||
public String updatePost(@PathVariable Long id,
|
||||
@Valid @ModelAttribute("post") Post post,
|
||||
BindingResult bindingResult,
|
||||
@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||
@RequestParam(value = "tagNames", required = false, defaultValue = "") String tagNames,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to update Post : {}", id);
|
||||
if (bindingResult.hasErrors()) {
|
||||
populateFormModel(model, post, tagNames, false);
|
||||
return "manage/posts/form";
|
||||
}
|
||||
post.setId(id);
|
||||
if (categoryId != null) {
|
||||
categoryService.findById(categoryId).ifPresent(post::setCategory);
|
||||
} else {
|
||||
post.setCategory(null);
|
||||
}
|
||||
postService.saveWithTags(post, tagNames);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Post updated successfully!");
|
||||
return "redirect:/manage/posts";
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /manage/posts/{id}/delete — Delete a post.
|
||||
*/
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deletePost(@PathVariable Long id, RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to delete Post : {}", id);
|
||||
postService.delete(id);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Post deleted successfully!");
|
||||
return "redirect:/manage/posts";
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate common model attributes for the post form.
|
||||
*/
|
||||
private void populateFormModel(Model model, Post post, String tagNames, boolean isNew) {
|
||||
model.addAttribute("post", post);
|
||||
model.addAttribute("tagNames", tagNames);
|
||||
model.addAttribute("isNew", isNew);
|
||||
model.addAttribute("categories", categoryService.findAll());
|
||||
model.addAttribute("statuses", PageStatus.values());
|
||||
model.addAttribute("layouts", PostLayout.values());
|
||||
model.addAttribute("allTags", tagService.findAll());
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package com.sisvietnamvn.web.controller.manage;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Tag;
|
||||
import com.sisvietnamvn.web.hook.HookManager;
|
||||
import com.sisvietnamvn.web.service.TagService;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
/**
|
||||
* Controller for managing post Tags in the admin panel.
|
||||
* Uses a WordPress-style split layout: form on the left, table on the right.
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/manage/posts/tags")
|
||||
public class ManageTagController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ManageTagController.class);
|
||||
|
||||
private final TagService tagService;
|
||||
private final HookManager hookManager;
|
||||
|
||||
public ManageTagController(TagService tagService, HookManager hookManager) {
|
||||
this.tagService = tagService;
|
||||
this.hookManager = hookManager;
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public void adminInit() {
|
||||
hookManager.doAction("admin_init");
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /manage/posts/tags — List all tags with inline create form.
|
||||
*/
|
||||
@GetMapping
|
||||
public String listTags(Model model) {
|
||||
LOG.debug("Request to list all tags");
|
||||
populateModel(model, new Tag(), true);
|
||||
return "manage/posts/tags";
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /manage/posts/tags — Save a new tag.
|
||||
*/
|
||||
@PostMapping
|
||||
public String createTag(@Valid @ModelAttribute("tag") Tag tag,
|
||||
BindingResult bindingResult,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to create Tag : {}", tag);
|
||||
if (bindingResult.hasErrors()) {
|
||||
populateModel(model, tag, true);
|
||||
return "manage/posts/tags";
|
||||
}
|
||||
tagService.save(tag);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Tag created successfully!");
|
||||
return "redirect:/manage/posts/tags";
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /manage/posts/tags/{id}/edit — Show the edit form for a tag.
|
||||
*/
|
||||
@GetMapping("/{id}/edit")
|
||||
public String showEditForm(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to show edit form for Tag : {}", id);
|
||||
Optional<Tag> tagOptional = tagService.findById(id);
|
||||
if (tagOptional.isEmpty()) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Tag not found.");
|
||||
return "redirect:/manage/posts/tags";
|
||||
}
|
||||
populateModel(model, tagOptional.get(), false);
|
||||
return "manage/posts/tags";
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /manage/posts/tags/{id} — Update an existing tag.
|
||||
*/
|
||||
@PostMapping("/{id}")
|
||||
public String updateTag(@PathVariable Long id,
|
||||
@Valid @ModelAttribute("tag") Tag tag,
|
||||
BindingResult bindingResult,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to update Tag : {}", id);
|
||||
if (bindingResult.hasErrors()) {
|
||||
populateModel(model, tag, false);
|
||||
return "manage/posts/tags";
|
||||
}
|
||||
tag.setId(id);
|
||||
tagService.save(tag);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Tag updated successfully!");
|
||||
return "redirect:/manage/posts/tags";
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /manage/posts/tags/{id}/delete — Delete a tag.
|
||||
*/
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deleteTag(@PathVariable Long id, RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to delete Tag : {}", id);
|
||||
tagService.delete(id);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Tag deleted successfully!");
|
||||
return "redirect:/manage/posts/tags";
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate common model attributes for the tags view.
|
||||
*/
|
||||
private void populateModel(Model model, Tag tag, boolean isNew) {
|
||||
model.addAttribute("tag", tag);
|
||||
model.addAttribute("isNew", isNew);
|
||||
model.addAttribute("allTags", tagService.findAll());
|
||||
// Build a map of tag ID -> post count for the table
|
||||
java.util.Map<Long, Long> postCounts = new java.util.HashMap<>();
|
||||
for (Tag t : tagService.findAll()) {
|
||||
postCounts.put(t.getId(), tagService.countPosts(t.getId()));
|
||||
}
|
||||
model.addAttribute("postCounts", postCounts);
|
||||
}
|
||||
}
|
||||
+8
@@ -74,4 +74,12 @@ public abstract class AbstractAuditingEntity<T> implements Serializable {
|
||||
public void setLastModifiedDate(Instant lastModifiedDate) {
|
||||
this.lastModifiedDate = lastModifiedDate;
|
||||
}
|
||||
|
||||
public java.util.Date getCreatedDateAsDate() {
|
||||
return createdDate != null ? java.util.Date.from(createdDate) : null;
|
||||
}
|
||||
|
||||
public java.util.Date getLastModifiedDateAsDate() {
|
||||
return lastModifiedDate != null ? java.util.Date.from(lastModifiedDate) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.sisvietnamvn.web.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.io.Serial;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A Category entity for classifying blog posts.
|
||||
* Supports hierarchical structure via self-referencing parent relationship.
|
||||
* Maps to the "sis_category" database table.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sis_category")
|
||||
public class Category extends AbstractAuditingEntity<Long> {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
@SequenceGenerator(name = "sequenceGenerator")
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 255)
|
||||
@Column(name = "name", length = 255, nullable = false)
|
||||
private String name;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 255)
|
||||
@Column(name = "slug", length = 255, nullable = false, unique = true)
|
||||
private String slug;
|
||||
|
||||
@Size(max = 1000)
|
||||
@Column(name = "description", length = 1000)
|
||||
private String description;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "parent_id")
|
||||
private Category parent;
|
||||
|
||||
// --- Getters and Setters ---
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getSlug() {
|
||||
return slug;
|
||||
}
|
||||
|
||||
public void setSlug(String slug) {
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Category getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public void setParent(Category parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
// --- equals, hashCode, toString ---
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Category category)) return false;
|
||||
return id != null && id.equals(category.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Category{" +
|
||||
"id=" + getId() +
|
||||
", name='" + getName() + "'" +
|
||||
", slug='" + getSlug() + "'" +
|
||||
", description='" + getDescription() + "'" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package com.sisvietnamvn.web.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.io.Serial;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A Post entity representing a blog/news article.
|
||||
* Maps to the "sis_post" database table.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sis_post")
|
||||
public class Post extends AbstractAuditingEntity<Long> {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
@SequenceGenerator(name = "sequenceGenerator")
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 255)
|
||||
@Column(name = "title", length = 255, nullable = false)
|
||||
private String title;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 255)
|
||||
@Column(name = "slug", length = 255, nullable = false, unique = true)
|
||||
private String slug;
|
||||
|
||||
@Lob
|
||||
@Column(name = "content")
|
||||
private String content;
|
||||
|
||||
@Size(max = 1000)
|
||||
@Column(name = "excerpt", length = 1000)
|
||||
private String excerpt;
|
||||
|
||||
@Size(max = 500)
|
||||
@Column(name = "featured_image", length = 500)
|
||||
private String featuredImage;
|
||||
|
||||
@NotNull
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", length = 20, nullable = false)
|
||||
private PageStatus status = PageStatus.DRAFT;
|
||||
|
||||
@Size(max = 500)
|
||||
@Column(name = "meta_description", length = 500)
|
||||
private String metaDescription;
|
||||
|
||||
@NotNull
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "layout", length = 20, nullable = false)
|
||||
private PostLayout layout = PostLayout.STANDARD;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "category_id")
|
||||
private Category category;
|
||||
|
||||
@ManyToMany(fetch = FetchType.LAZY)
|
||||
@JoinTable(
|
||||
name = "sis_post_tag",
|
||||
joinColumns = @JoinColumn(name = "post_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "tag_id")
|
||||
)
|
||||
private Set<Tag> tags = new HashSet<>();
|
||||
|
||||
// --- Getters and Setters ---
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getSlug() {
|
||||
return slug;
|
||||
}
|
||||
|
||||
public void setSlug(String slug) {
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getExcerpt() {
|
||||
return excerpt;
|
||||
}
|
||||
|
||||
public void setExcerpt(String excerpt) {
|
||||
this.excerpt = excerpt;
|
||||
}
|
||||
|
||||
public String getFeaturedImage() {
|
||||
return featuredImage;
|
||||
}
|
||||
|
||||
public void setFeaturedImage(String featuredImage) {
|
||||
this.featuredImage = featuredImage;
|
||||
}
|
||||
|
||||
public PageStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(PageStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getMetaDescription() {
|
||||
return metaDescription;
|
||||
}
|
||||
|
||||
public void setMetaDescription(String metaDescription) {
|
||||
this.metaDescription = metaDescription;
|
||||
}
|
||||
|
||||
public PostLayout getLayout() {
|
||||
return layout;
|
||||
}
|
||||
|
||||
public void setLayout(PostLayout layout) {
|
||||
this.layout = layout;
|
||||
}
|
||||
|
||||
public Category getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public void setCategory(Category category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public Set<Tag> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
public void setTags(Set<Tag> tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
// --- equals, hashCode, toString ---
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Post post)) return false;
|
||||
return id != null && id.equals(post.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Post{" +
|
||||
"id=" + getId() +
|
||||
", title='" + getTitle() + "'" +
|
||||
", slug='" + getSlug() + "'" +
|
||||
", status='" + getStatus() + "'" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.sisvietnamvn.web.domain;
|
||||
|
||||
/**
|
||||
* Enumeration for Post Layout options.
|
||||
*/
|
||||
public enum PostLayout {
|
||||
STANDARD,
|
||||
SIDEBAR,
|
||||
FULL_WIDTH
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.sisvietnamvn.web.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.io.Serial;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A Tag entity for labeling blog posts with keywords.
|
||||
* Maps to the "sis_tag" database table.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sis_tag")
|
||||
public class Tag extends AbstractAuditingEntity<Long> {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
@SequenceGenerator(name = "sequenceGenerator")
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 255)
|
||||
@Column(name = "name", length = 255, nullable = false, unique = true)
|
||||
private String name;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 255)
|
||||
@Column(name = "slug", length = 255, nullable = false, unique = true)
|
||||
private String slug;
|
||||
|
||||
// --- Getters and Setters ---
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getSlug() {
|
||||
return slug;
|
||||
}
|
||||
|
||||
public void setSlug(String slug) {
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
// --- equals, hashCode, toString ---
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Tag tag)) return false;
|
||||
return id != null && id.equals(tag.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Tag{" +
|
||||
"id=" + getId() +
|
||||
", name='" + getName() + "'" +
|
||||
", slug='" + getSlug() + "'" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.sisvietnamvn.web.repository;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Category;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Spring Data JPA repository for the {@link Category} entity.
|
||||
*/
|
||||
@Repository
|
||||
public interface CategoryRepository extends JpaRepository<Category, Long> {
|
||||
|
||||
/**
|
||||
* Find all categories ordered by name ascending.
|
||||
*/
|
||||
List<Category> findAllByOrderByNameAsc();
|
||||
|
||||
/**
|
||||
* Find a category by its URL-friendly slug.
|
||||
*/
|
||||
Optional<Category> findBySlug(String slug);
|
||||
|
||||
/**
|
||||
* Check if a slug already exists (for uniqueness validation on create).
|
||||
*/
|
||||
boolean existsBySlug(String slug);
|
||||
|
||||
/**
|
||||
* Check if a slug already exists for a different category (for edit validation).
|
||||
*/
|
||||
boolean existsBySlugAndIdNot(String slug, Long id);
|
||||
|
||||
/**
|
||||
* Find all top-level categories (no parent).
|
||||
*/
|
||||
List<Category> findByParentIsNullOrderByNameAsc();
|
||||
|
||||
/**
|
||||
* Find child categories of a given parent.
|
||||
*/
|
||||
List<Category> findByParentIdOrderByNameAsc(Long parentId);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.sisvietnamvn.web.repository;
|
||||
|
||||
import com.sisvietnamvn.web.domain.PageStatus;
|
||||
import com.sisvietnamvn.web.domain.Post;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Spring Data JPA repository for the {@link Post} entity.
|
||||
*/
|
||||
@Repository
|
||||
public interface PostRepository extends JpaRepository<Post, Long> {
|
||||
|
||||
/**
|
||||
* Find all posts ordered by creation date descending (newest first).
|
||||
*/
|
||||
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
|
||||
List<Post> findAllByOrderByCreatedDateDesc();
|
||||
|
||||
/**
|
||||
* Find a post by its ID with eagerly fetched category and tags.
|
||||
*/
|
||||
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
|
||||
Optional<Post> findById(Long id);
|
||||
|
||||
/**
|
||||
* Find a post by its URL-friendly slug.
|
||||
*/
|
||||
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
|
||||
Optional<Post> findBySlug(String slug);
|
||||
|
||||
/**
|
||||
* Find all posts with a given publication status.
|
||||
*/
|
||||
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
|
||||
List<Post> findByStatusOrderByCreatedDateDesc(PageStatus status);
|
||||
|
||||
/**
|
||||
* Find all posts in a given category.
|
||||
*/
|
||||
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
|
||||
List<Post> findByCategoryIdOrderByCreatedDateDesc(Long categoryId);
|
||||
|
||||
/**
|
||||
* Find all posts in a given category with a given status.
|
||||
*/
|
||||
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
|
||||
List<Post> findByCategoryIdAndStatusOrderByCreatedDateDesc(Long categoryId, PageStatus status);
|
||||
|
||||
/**
|
||||
* Count the number of posts in a given category.
|
||||
*/
|
||||
long countByCategoryId(Long categoryId);
|
||||
|
||||
/**
|
||||
* Count the number of posts associated with a given tag.
|
||||
*/
|
||||
@Query("SELECT COUNT(p) FROM Post p JOIN p.tags t WHERE t.id = :tagId")
|
||||
long countByTagId(@Param("tagId") Long tagId);
|
||||
|
||||
/**
|
||||
* Check if a slug already exists (for uniqueness validation on create).
|
||||
*/
|
||||
boolean existsBySlug(String slug);
|
||||
|
||||
/**
|
||||
* Check if a slug already exists for a different post (for edit validation).
|
||||
*/
|
||||
boolean existsBySlugAndIdNot(String slug, Long id);
|
||||
|
||||
/**
|
||||
* Find all posts that have a specific tag.
|
||||
*/
|
||||
@Query("SELECT p FROM Post p JOIN p.tags t WHERE t.id = :tagId ORDER BY p.createdDate DESC")
|
||||
List<Post> findByTagId(@Param("tagId") Long tagId);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.sisvietnamvn.web.repository;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Tag;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Spring Data JPA repository for the {@link Tag} entity.
|
||||
*/
|
||||
@Repository
|
||||
public interface TagRepository extends JpaRepository<Tag, Long> {
|
||||
|
||||
/**
|
||||
* Find all tags ordered by name ascending.
|
||||
*/
|
||||
List<Tag> findAllByOrderByNameAsc();
|
||||
|
||||
/**
|
||||
* Find a tag by its URL-friendly slug.
|
||||
*/
|
||||
Optional<Tag> findBySlug(String slug);
|
||||
|
||||
/**
|
||||
* Find a tag by its exact name.
|
||||
*/
|
||||
Optional<Tag> findByName(String name);
|
||||
|
||||
/**
|
||||
* Search tags by name containing the given string (for autocomplete).
|
||||
*/
|
||||
List<Tag> findByNameContainingIgnoreCase(String name);
|
||||
|
||||
/**
|
||||
* Check if a slug already exists (for uniqueness validation on create).
|
||||
*/
|
||||
boolean existsBySlug(String slug);
|
||||
|
||||
/**
|
||||
* Check if a slug already exists for a different tag (for edit validation).
|
||||
*/
|
||||
boolean existsBySlugAndIdNot(String slug, Long id);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.sisvietnamvn.web.service;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Category;
|
||||
import com.sisvietnamvn.web.repository.CategoryRepository;
|
||||
import com.sisvietnamvn.web.repository.PostRepository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Service class for managing post categories.
|
||||
*/
|
||||
@Service
|
||||
@Transactional
|
||||
public class CategoryService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CategoryService.class);
|
||||
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final PostRepository postRepository;
|
||||
|
||||
public CategoryService(CategoryRepository categoryRepository, PostRepository postRepository) {
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.postRepository = postRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories sorted by name.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Category> findAll() {
|
||||
LOG.debug("Request to get all Categories");
|
||||
return categoryRepository.findAllByOrderByNameAsc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single category by ID.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Category> findById(Long id) {
|
||||
LOG.debug("Request to get Category : {}", id);
|
||||
return categoryRepository.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single category by its URL slug.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Category> findBySlug(String slug) {
|
||||
LOG.debug("Request to get Category by slug : {}", slug);
|
||||
return categoryRepository.findBySlug(slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top-level categories (no parent).
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Category> findTopLevel() {
|
||||
LOG.debug("Request to get top-level Categories");
|
||||
return categoryRepository.findByParentIsNullOrderByNameAsc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of posts in a category.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long countPosts(Long categoryId) {
|
||||
return postRepository.countByCategoryId(categoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a category (create or update).
|
||||
* Auto-generates a URL slug from the name if the slug is empty.
|
||||
*/
|
||||
public Category save(Category category) {
|
||||
LOG.debug("Request to save Category : {}", category);
|
||||
|
||||
if (category.getSlug() == null || category.getSlug().isBlank()) {
|
||||
category.setSlug(PageService.generateSlug(category.getName()));
|
||||
}
|
||||
|
||||
// Ensure slug uniqueness
|
||||
String baseSlug = category.getSlug();
|
||||
String candidateSlug = baseSlug;
|
||||
int counter = 1;
|
||||
while (isSlugTaken(candidateSlug, category.getId())) {
|
||||
candidateSlug = baseSlug + "-" + counter;
|
||||
counter++;
|
||||
}
|
||||
category.setSlug(candidateSlug);
|
||||
|
||||
return categoryRepository.save(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a category by ID.
|
||||
* @throws IllegalStateException if the category still has posts assigned to it.
|
||||
*/
|
||||
public void delete(Long id) {
|
||||
LOG.debug("Request to delete Category : {}", id);
|
||||
long postCount = postRepository.countByCategoryId(id);
|
||||
if (postCount > 0) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot delete category: " + postCount + " post(s) are still assigned to it. " +
|
||||
"Please reassign or delete those posts first."
|
||||
);
|
||||
}
|
||||
categoryRepository.deleteById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a slug is already taken by another category.
|
||||
*/
|
||||
private boolean isSlugTaken(String slug, Long currentCategoryId) {
|
||||
if (currentCategoryId == null) {
|
||||
return categoryRepository.existsBySlug(slug);
|
||||
}
|
||||
return categoryRepository.existsBySlugAndIdNot(slug, currentCategoryId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.sisvietnamvn.web.service;
|
||||
|
||||
import com.sisvietnamvn.web.domain.PageStatus;
|
||||
import com.sisvietnamvn.web.domain.Post;
|
||||
import com.sisvietnamvn.web.domain.Tag;
|
||||
import com.sisvietnamvn.web.hook.HookManager;
|
||||
import com.sisvietnamvn.web.repository.PostRepository;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Service class for managing blog posts.
|
||||
*/
|
||||
@Service
|
||||
@Transactional
|
||||
public class PostService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PostService.class);
|
||||
|
||||
private final PostRepository postRepository;
|
||||
private final TagService tagService;
|
||||
private final HookManager hookManager;
|
||||
|
||||
public PostService(PostRepository postRepository, TagService tagService, HookManager hookManager) {
|
||||
this.postRepository = postRepository;
|
||||
this.tagService = tagService;
|
||||
this.hookManager = hookManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all posts ordered by creation date (newest first).
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Post> findAll() {
|
||||
LOG.debug("Request to get all Posts");
|
||||
return postRepository.findAllByOrderByCreatedDateDesc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single post by ID.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Post> findById(Long id) {
|
||||
LOG.debug("Request to get Post : {}", id);
|
||||
return postRepository.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single post by its URL slug.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Post> findBySlug(String slug) {
|
||||
LOG.debug("Request to get Post by slug : {}", slug);
|
||||
return postRepository.findBySlug(slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all posts with a specific status.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Post> findByStatus(PageStatus status) {
|
||||
LOG.debug("Request to get Posts by status : {}", status);
|
||||
return postRepository.findByStatusOrderByCreatedDateDesc(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all posts in a specific category.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Post> findByCategoryId(Long categoryId) {
|
||||
LOG.debug("Request to get Posts by category : {}", categoryId);
|
||||
return postRepository.findByCategoryIdOrderByCreatedDateDesc(categoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all posts filtered by category and/or status.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Post> findFiltered(Long categoryId, PageStatus status) {
|
||||
LOG.debug("Request to get Posts filtered by categoryId={}, status={}", categoryId, status);
|
||||
if (categoryId != null && status != null) {
|
||||
return postRepository.findByCategoryIdAndStatusOrderByCreatedDateDesc(categoryId, status);
|
||||
} else if (categoryId != null) {
|
||||
return postRepository.findByCategoryIdOrderByCreatedDateDesc(categoryId);
|
||||
} else if (status != null) {
|
||||
return postRepository.findByStatusOrderByCreatedDateDesc(status);
|
||||
}
|
||||
return postRepository.findAllByOrderByCreatedDateDesc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a post (create or update).
|
||||
* Auto-generates a URL slug from the title if the slug is empty.
|
||||
*/
|
||||
public Post save(Post post) {
|
||||
LOG.debug("Request to save Post : {}", post);
|
||||
|
||||
// Hook: pre_save_post filter
|
||||
post = hookManager.applyFilters("pre_save_post", post);
|
||||
|
||||
if (post.getSlug() == null || post.getSlug().isBlank()) {
|
||||
post.setSlug(PageService.generateSlug(post.getTitle()));
|
||||
}
|
||||
|
||||
// Ensure slug uniqueness
|
||||
String baseSlug = post.getSlug();
|
||||
String candidateSlug = baseSlug;
|
||||
int counter = 1;
|
||||
while (isSlugTaken(candidateSlug, post.getId())) {
|
||||
candidateSlug = baseSlug + "-" + counter;
|
||||
counter++;
|
||||
}
|
||||
|
||||
// Hook: post_slug filter
|
||||
candidateSlug = hookManager.applyFilters("post_slug", candidateSlug);
|
||||
post.setSlug(candidateSlug);
|
||||
|
||||
Post savedPost = postRepository.save(post);
|
||||
|
||||
// Hook: save_post action
|
||||
hookManager.doAction("save_post", savedPost);
|
||||
|
||||
return savedPost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a post and process comma-separated tag names.
|
||||
* Auto-creates tags that don't exist yet.
|
||||
*
|
||||
* @param post the post entity
|
||||
* @param tagNames comma-separated tag names (e.g. "news, health, update")
|
||||
*/
|
||||
public Post saveWithTags(Post post, String tagNames) {
|
||||
Set<Tag> tags = new HashSet<>();
|
||||
if (tagNames != null && !tagNames.isBlank()) {
|
||||
String[] names = tagNames.split(",");
|
||||
for (String name : names) {
|
||||
String trimmed = name.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
tags.add(tagService.findOrCreateByName(trimmed));
|
||||
}
|
||||
}
|
||||
}
|
||||
post.setTags(tags);
|
||||
return save(post);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a post by ID.
|
||||
*/
|
||||
public void delete(Long id) {
|
||||
LOG.debug("Request to delete Post : {}", id);
|
||||
postRepository.deleteById(id);
|
||||
|
||||
// Hook: deleted_post action
|
||||
hookManager.doAction("deleted_post", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a slug is already taken by another post.
|
||||
*/
|
||||
private boolean isSlugTaken(String slug, Long currentPostId) {
|
||||
if (currentPostId == null) {
|
||||
return postRepository.existsBySlug(slug);
|
||||
}
|
||||
return postRepository.existsBySlugAndIdNot(slug, currentPostId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.sisvietnamvn.web.service;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Tag;
|
||||
import com.sisvietnamvn.web.repository.PostRepository;
|
||||
import com.sisvietnamvn.web.repository.TagRepository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Service class for managing post tags.
|
||||
*/
|
||||
@Service
|
||||
@Transactional
|
||||
public class TagService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TagService.class);
|
||||
|
||||
private final TagRepository tagRepository;
|
||||
private final PostRepository postRepository;
|
||||
|
||||
public TagService(TagRepository tagRepository, PostRepository postRepository) {
|
||||
this.tagRepository = tagRepository;
|
||||
this.postRepository = postRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags sorted by name.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Tag> findAll() {
|
||||
LOG.debug("Request to get all Tags");
|
||||
return tagRepository.findAllByOrderByNameAsc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single tag by ID.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Tag> findById(Long id) {
|
||||
LOG.debug("Request to get Tag : {}", id);
|
||||
return tagRepository.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single tag by its URL slug.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Tag> findBySlug(String slug) {
|
||||
LOG.debug("Request to get Tag by slug : {}", slug);
|
||||
return tagRepository.findBySlug(slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create a tag by name.
|
||||
* Used when processing comma-separated tag input from the post editor.
|
||||
*/
|
||||
public Tag findOrCreateByName(String name) {
|
||||
String trimmed = name.trim();
|
||||
LOG.debug("Request to find or create Tag by name : {}", trimmed);
|
||||
return tagRepository.findByName(trimmed)
|
||||
.orElseGet(() -> {
|
||||
Tag tag = new Tag();
|
||||
tag.setName(trimmed);
|
||||
tag.setSlug(PageService.generateSlug(trimmed));
|
||||
return save(tag);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search tags by name (for autocomplete).
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Tag> searchByName(String query) {
|
||||
LOG.debug("Request to search Tags by name : {}", query);
|
||||
return tagRepository.findByNameContainingIgnoreCase(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of posts associated with a tag.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long countPosts(Long tagId) {
|
||||
return postRepository.countByTagId(tagId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a tag (create or update).
|
||||
* Auto-generates a URL slug from the name if the slug is empty.
|
||||
*/
|
||||
public Tag save(Tag tag) {
|
||||
LOG.debug("Request to save Tag : {}", tag);
|
||||
|
||||
if (tag.getSlug() == null || tag.getSlug().isBlank()) {
|
||||
tag.setSlug(PageService.generateSlug(tag.getName()));
|
||||
}
|
||||
|
||||
// Ensure slug uniqueness
|
||||
String baseSlug = tag.getSlug();
|
||||
String candidateSlug = baseSlug;
|
||||
int counter = 1;
|
||||
while (isSlugTaken(candidateSlug, tag.getId())) {
|
||||
candidateSlug = baseSlug + "-" + counter;
|
||||
counter++;
|
||||
}
|
||||
tag.setSlug(candidateSlug);
|
||||
|
||||
return tagRepository.save(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tag by ID.
|
||||
*/
|
||||
public void delete(Long id) {
|
||||
LOG.debug("Request to delete Tag : {}", id);
|
||||
tagRepository.deleteById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a slug is already taken by another tag.
|
||||
*/
|
||||
private boolean isSlugTaken(String slug, Long currentTagId) {
|
||||
if (currentTagId == null) {
|
||||
return tagRepository.existsBySlug(slug);
|
||||
}
|
||||
return tagRepository.existsBySlugAndIdNot(slug, currentTagId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user