Большой рефакторинг

This commit is contained in:
Struchkov Mark 2022-09-25 09:31:33 +03:00
parent ebf825b448
commit 7453c53be6
33 changed files with 821 additions and 677 deletions

21
pom.xml
View File

@ -5,16 +5,22 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.2</version>
<version>2.7.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yapract</groupId>
<groupId>dev.struchkov.example</groupId>
<artifactId>spring-boot-one-to-many</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-one-to-many</name>
<properties>
<java.version>1.8</java.version>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
@ -29,14 +35,13 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.4.0</version>
<version>42.5.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

View File

@ -1,13 +0,0 @@
package com.yprac.spring.hibernate.onetomany;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootOneToManyApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootOneToManyApplication.class, args);
}
}

View File

@ -1,89 +0,0 @@
package com.yprac.spring.hibernate.onetomany.controller;
import java.util.List;
import com.yprac.spring.hibernate.onetomany.exception.ResourceNotFoundException;
import com.yprac.spring.hibernate.onetomany.repository.CommentRepository;
import com.yprac.spring.hibernate.onetomany.repository.TutorialRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
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.RestController;
import com.yprac.spring.hibernate.onetomany.model.Comment;
@CrossOrigin(origins = "http://localhost:8081")
@RestController
@RequestMapping("/api")
public class CommentController {
@Autowired
private TutorialRepository tutorialRepository;
@Autowired
private CommentRepository commentRepository;
@GetMapping("/tutorials/{tutorialId}/comments")
public ResponseEntity<List<Comment>> getAllCommentsByTutorialId(@PathVariable(value = "tutorialId") Long tutorialId) {
if (!tutorialRepository.existsById(tutorialId)) {
throw new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId);
}
List<Comment> comments = commentRepository.findByTutorialId(tutorialId);
return new ResponseEntity<>(comments, HttpStatus.OK);
}
@GetMapping("/comments/{id}")
public ResponseEntity<Comment> getCommentsByTutorialId(@PathVariable(value = "id") Long id) {
Comment comment = commentRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Not found Comment with id = " + id));
return new ResponseEntity<>(comment, HttpStatus.OK);
}
@PostMapping("/tutorials/{tutorialId}/comments")
public ResponseEntity<Comment> createComment(@PathVariable(value = "tutorialId") Long tutorialId,
@RequestBody Comment commentRequest) {
Comment comment = tutorialRepository.findById(tutorialId).map(tutorial -> {
commentRequest.setTutorial(tutorial);
return commentRepository.save(commentRequest);
}).orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId));
return new ResponseEntity<>(comment, HttpStatus.CREATED);
}
@PutMapping("/comments/{id}")
public ResponseEntity<Comment> updateComment(@PathVariable("id") long id, @RequestBody Comment commentRequest) {
Comment comment = commentRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("CommentId " + id + "not found"));
comment.setContent(commentRequest.getContent());
return new ResponseEntity<>(commentRepository.save(comment), HttpStatus.OK);
}
@DeleteMapping("/comments/{id}")
public ResponseEntity<HttpStatus> deleteComment(@PathVariable("id") long id) {
commentRepository.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@DeleteMapping("/tutorials/{tutorialId}/comments")
public ResponseEntity<List<Comment>> deleteAllCommentsOfTutorial(@PathVariable(value = "tutorialId") Long tutorialId) {
if (!tutorialRepository.existsById(tutorialId)) {
throw new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId);
}
commentRepository.deleteByTutorialId(tutorialId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}

View File

@ -1,104 +0,0 @@
package com.yprac.spring.hibernate.onetomany.controller;
import com.yprac.spring.hibernate.onetomany.exception.ResourceNotFoundException;
import com.yprac.spring.hibernate.onetomany.model.Tag;
import com.yprac.spring.hibernate.onetomany.model.Tutorial;
import com.yprac.spring.hibernate.onetomany.repository.TagRepository;
import com.yprac.spring.hibernate.onetomany.repository.TutorialRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@CrossOrigin(origins = "http://localhost:8081")
@RestController
@RequestMapping("/api")
public class TagController {
@Autowired
private TutorialRepository tutorialRepository;
@Autowired
private TagRepository tagRepository;
@GetMapping("/tags")
public ResponseEntity<List<Tag>> getAllTags() {
List<Tag> tags = new ArrayList<Tag>();
tagRepository.findAll().forEach(tags::add);
if (tags.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(tags, HttpStatus.OK);
}
@GetMapping("/tutorials/{tutorialId}/tags")
public ResponseEntity<List<Tag>> getAllTagsByTutorialId(@PathVariable(value = "tutorialId") Long tutorialId) {
if (!tutorialRepository.existsById(tutorialId)) {
throw new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId);
}
List<Tag> tags = tagRepository.findTagsByTutorialsId(tutorialId);
return new ResponseEntity<>(tags, HttpStatus.OK);
}
@GetMapping("/tags/{id}")
public ResponseEntity<Tag> getTagsById(@PathVariable(value = "id") Long id) {
Tag tag = tagRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Not found Tag with id = " + id));
return new ResponseEntity<>(tag, HttpStatus.OK);
}
@GetMapping("/tags/{tagId}/tutorials")
public ResponseEntity<List<Tutorial>> getAllTutorialsByTagId(@PathVariable(value = "tagId") Long tagId) {
if (!tagRepository.existsById(tagId)) {
throw new ResourceNotFoundException("Not found Tag with id = " + tagId);
}
List<Tutorial> tutorials = tutorialRepository.findTutorialsByTagsId(tagId);
return new ResponseEntity<>(tutorials, HttpStatus.OK);
}
@PostMapping("/tutorials/{tutorialId}/tags")
public ResponseEntity<Tag> addTag(@PathVariable(value = "tutorialId") Long tutorialId, @RequestBody Tag tagRequest) {
Tag tag = tutorialRepository.findById(tutorialId).map(tutorial -> {
long tagId = tagRequest.getId();
// tag is existed
if (tagId != 0L) {
Tag _tag = tagRepository.findById(tagId)
.orElseThrow(() -> new ResourceNotFoundException("Not found Tag with id = " + tagId));
tutorial.addTag(_tag);
tutorialRepository.save(tutorial);
return _tag;
}
// add and create new Tag
tutorial.addTag(tagRequest);
return tagRepository.save(tagRequest);
}).orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId));
return new ResponseEntity<>(tag, HttpStatus.CREATED);
}
@PutMapping("/tags/{id}")
public ResponseEntity<Tag> updateTag(@PathVariable("id") long id, @RequestBody Tag tagRequest) {
Tag tag = tagRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("TagId " + id + "not found"));
tag.setName(tagRequest.getName());
return new ResponseEntity<>(tagRepository.save(tag), HttpStatus.OK);
}
@DeleteMapping("/tutorials/{tutorialId}/tags/{tagId}")
public ResponseEntity<HttpStatus> deleteTagFromTutorial(@PathVariable(value = "tutorialId") Long tutorialId, @PathVariable(value = "tagId") Long tagId) {
Tutorial tutorial = tutorialRepository.findById(tutorialId)
.orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId));
tutorial.removeTag(tagId);
tutorialRepository.save(tutorial);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@DeleteMapping("/tags/{id}")
public ResponseEntity<HttpStatus> deleteTag(@PathVariable("id") long id) {
tagRepository.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}

View File

@ -1,98 +0,0 @@
package com.yprac.spring.hibernate.onetomany.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
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 com.yprac.spring.hibernate.onetomany.exception.ResourceNotFoundException;
import com.yprac.spring.hibernate.onetomany.model.Tutorial;
import com.yprac.spring.hibernate.onetomany.repository.TutorialRepository;
@CrossOrigin(origins = "http://localhost:8081")
@RestController
@RequestMapping("/api")
public class TutorialController {
@Autowired
TutorialRepository tutorialRepository;
@GetMapping("/tutorials")
public ResponseEntity<List<Tutorial>> getAllTutorials(@RequestParam(required = false) String title) {
List<Tutorial> tutorials = new ArrayList<Tutorial>();
if (title == null)
tutorialRepository.findAll().forEach(tutorials::add);
else
tutorialRepository.findByTitleContaining(title).forEach(tutorials::add);
if (tutorials.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(tutorials, HttpStatus.OK);
}
@GetMapping("/tutorials/{id}")
public ResponseEntity<Tutorial> getTutorialById(@PathVariable("id") long id) {
Tutorial tutorial = tutorialRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + id));
return new ResponseEntity<>(tutorial, HttpStatus.OK);
}
@PostMapping("/tutorials")
public ResponseEntity<Tutorial> createTutorial(@RequestBody Tutorial tutorial) {
Tutorial _tutorial = tutorialRepository.save(new Tutorial(tutorial.getTitle(), tutorial.getDescription(), true));
return new ResponseEntity<>(_tutorial, HttpStatus.CREATED);
}
@PutMapping("/tutorials/{id}")
public ResponseEntity<Tutorial> updateTutorial(@PathVariable("id") long id, @RequestBody Tutorial tutorial) {
Tutorial _tutorial = tutorialRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + id));
_tutorial.setTitle(tutorial.getTitle());
_tutorial.setDescription(tutorial.getDescription());
_tutorial.setPublished(tutorial.isPublished());
return new ResponseEntity<>(tutorialRepository.save(_tutorial), HttpStatus.OK);
}
@DeleteMapping("/tutorials/{id}")
public ResponseEntity<HttpStatus> deleteTutorial(@PathVariable("id") long id) {
tutorialRepository.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@DeleteMapping("/tutorials")
public ResponseEntity<HttpStatus> deleteAllTutorials() {
tutorialRepository.deleteAll();
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@GetMapping("/tutorials/published")
public ResponseEntity<List<Tutorial>> findByPublished() {
List<Tutorial> tutorials = tutorialRepository.findByPublished(true);
if (tutorials.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(tutorials, HttpStatus.OK);
}
}

View File

@ -1,37 +0,0 @@
package com.yprac.spring.hibernate.onetomany.exception;
import java.util.Date;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
@RestControllerAdvice
public class ControllerExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public ErrorMessage resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
ErrorMessage message = new ErrorMessage(
HttpStatus.NOT_FOUND.value(),
new Date(),
ex.getMessage(),
request.getDescription(false));
return message;
}
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorMessage globalExceptionHandler(Exception ex, WebRequest request) {
ErrorMessage message = new ErrorMessage(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
new Date(),
ex.getMessage(),
request.getDescription(false));
return message;
}
}

View File

@ -1,33 +0,0 @@
package com.yprac.spring.hibernate.onetomany.exception;
import java.util.Date;
public class ErrorMessage {
private int statusCode;
private Date timestamp;
private String message;
private String description;
public ErrorMessage(int statusCode, Date timestamp, String message, String description) {
this.statusCode = statusCode;
this.timestamp = timestamp;
this.message = message;
this.description = description;
}
public int getStatusCode() {
return statusCode;
}
public Date getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public String getDescription() {
return description;
}
}

View File

@ -1,10 +0,0 @@
package com.yprac.spring.hibernate.onetomany.exception;
public class ResourceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ResourceNotFoundException(String msg) {
super(msg);
}
}

View File

@ -1,46 +0,0 @@
package com.yprac.spring.hibernate.onetomany.model;
import javax.persistence.*;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = "comments")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "comment_generator")
private Long id;
@Column(name = "content")
private String content;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "tutorial_id", nullable = false)
// @OnDelete(action = OnDeleteAction.CASCADE)
@JsonIgnore
private Tutorial tutorial;
public Long getId() {
return id;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Tutorial getTutorial() {
return tutorial;
}
public void setTutorial(Tutorial tutorial) {
this.tutorial = tutorial;
}
}

View File

@ -1,36 +0,0 @@
package com.yprac.spring.hibernate.onetomany.model;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = "tags")
public class Tag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name")
private String name;
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "tags")
@JsonIgnore
private Set<Tutorial> tutorials = new HashSet<>();
public Tag() {
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Tutorial> getTutorials() {
return tutorials;
}
public void setTutorials(Set<Tutorial> tutorials) {
this.tutorials = tutorials;
}
}

View File

@ -1,108 +0,0 @@
package com.yprac.spring.hibernate.onetomany.model;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "tutorials")
public class Tutorial {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tutorial_generator")
private long id;
@Column(name = "title")
private String title;
@Column(name = "description")
private String description;
@Column(name = "published")
private boolean published;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
@JoinColumn(name = "tutorial_id")
private Set<Comment> comments = new HashSet<>();
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(name = "tutorial_tags",
joinColumns = { @JoinColumn(name = "tutorial_id") },
inverseJoinColumns = { @JoinColumn(name = "tag_id") })
private Set<Tag> tags = new HashSet<>();
public Tutorial() {
}
public Tutorial(String title, String description, boolean published) {
this.title = title;
this.description = description;
this.published = published;
}
public long getId() {
return id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isPublished() {
return published;
}
public void setPublished(boolean isPublished) {
this.published = isPublished;
}
public Set<Tag> getTags() {
return tags;
}
public void setTags(Set<Tag> tags) {
this.tags = tags;
}
public Set<Comment> getComments() {
return comments;
}
public void setComments(Set<Comment> comments) {
this.comments = comments;
}
@Override
public String toString() {
return "Tutorial [id=" + id + ", title=" + title + ", desc=" + description + ", published=" + published + "]";
}
public void addTag(Tag tag) {
this.tags.add(tag);
tag.getTutorials().add(this);
}
public void removeTag(Long tagId) {
Tag tag = this.tags.stream().filter(t -> t.getId() == tagId).findFirst().orElse(null);
if (tag != null) {
this.tags.remove(tag);
tag.getTutorials().remove(this);
}
}
}

View File

@ -1,16 +0,0 @@
package com.yprac.spring.hibernate.onetomany.repository;
import java.util.List;
import javax.transaction.Transactional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.yprac.spring.hibernate.onetomany.model.Comment;
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByTutorialId(Long postId);
@Transactional
void deleteByTutorialId(long tutorialId);
}

View File

@ -1,15 +0,0 @@
package com.yprac.spring.hibernate.onetomany.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.yprac.spring.hibernate.onetomany.model.Tutorial;
public interface TutorialRepository extends JpaRepository<Tutorial, Long> {
List<Tutorial> findByPublished(boolean published);
List<Tutorial> findByTitleContaining(String title);
List<Tutorial> findTutorialsByTagsId(Long tagId);
}

View File

@ -0,0 +1,13 @@
package dev.struchkov.example.portal;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootApp {
public static void main(String[] args) {
SpringApplication.run(SpringBootApp.class, args);
}
}

View File

@ -0,0 +1,75 @@
package dev.struchkov.example.portal.controller;
import dev.struchkov.example.portal.manager.TutorialManager;
import dev.struchkov.example.portal.model.Comment;
import dev.struchkov.example.portal.service.CommentService;
import lombok.RequiredArgsConstructor;
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.RestController;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class CommentController {
private final TutorialManager tutorialManager;
private final CommentService commentService;
@GetMapping("/tutorials/{tutorialId}/comments")
public ResponseEntity<List<Comment>> getAllCommentsByTutorialId(
@PathVariable(value = "tutorialId") Long tutorialId
) {
return ResponseEntity.ok(commentService.getByTutorialId(tutorialId));
}
@GetMapping("/comments/{commentId}")
public ResponseEntity<Comment> getCommentsByTutorialId(
@PathVariable(value = "commentId") Long commentId
) {
return ResponseEntity.ok(commentService.getByIdOrThrow(commentId));
}
@PostMapping("/tutorials/{tutorialId}/comments")
public ResponseEntity<Comment> createComment(
@PathVariable(value = "tutorialId") Long tutorialId,
@RequestBody Comment newComment
) {
return ResponseEntity.ok(tutorialManager.addNewComment(tutorialId, newComment));
}
@PutMapping("/comments/{commentId}")
public ResponseEntity<Comment> updateComment(
@PathVariable("commentId") Long id,
@RequestBody Comment updateComment
) {
return ResponseEntity.ok(commentService.update(id, updateComment));
}
@DeleteMapping("/comments/{commentId}")
public ResponseEntity<HttpStatus> deleteComment(
@PathVariable("commentId") Long commentId
) {
commentService.deleteById(commentId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
@DeleteMapping("/tutorials/{tutorialId}/comments")
public ResponseEntity<List<Comment>> deleteAllCommentsOfTutorial(
@PathVariable(value = "tutorialId") Long tutorialId
) {
commentService.deleteAllByTutorialId(tutorialId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
}

View File

@ -0,0 +1,93 @@
package dev.struchkov.example.portal.controller;
import dev.struchkov.example.portal.manager.TutorialManager;
import dev.struchkov.example.portal.model.Tag;
import dev.struchkov.example.portal.model.Tutorial;
import dev.struchkov.example.portal.service.TagService;
import dev.struchkov.example.portal.service.TutorialService;
import lombok.RequiredArgsConstructor;
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.RestController;
import java.util.List;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class TagController {
private final TutorialManager tutorialManager;
private final TutorialService tutorialService;
private final TagService tagService;
@GetMapping("/tags")
public ResponseEntity<List<Tag>> getAllTags() {
final List<Tag> tags = tagService.getAll();
if (tags.isEmpty()) {
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
return ResponseEntity.ok(tags);
}
@GetMapping("/tutorials/{tutorialId}/tags")
public ResponseEntity<List<Tag>> getAllTagsByTutorialId(@PathVariable(value = "tutorialId") Long tutorialId) {
final List<Tag> tags = tagService.getAllByTutorialId(tutorialId);
if (tags.isEmpty()) {
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
return ResponseEntity.ok(tags);
}
@GetMapping("/tags/{tagId}")
public ResponseEntity<Tag> getTagsById(@PathVariable(value = "tagId") Long tagId) {
return ResponseEntity.ok(tagService.getByIdOrThrow(tagId));
}
@GetMapping("/tags/{tagId}/tutorials")
public ResponseEntity<List<Tutorial>> getAllTutorialsByTagId(@PathVariable(value = "tagId") Long tagId) {
final List<Tutorial> tutorials = tutorialService.getAllByTagId(tagId);
if (tutorials.isEmpty()) {
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
return ResponseEntity.ok(tutorials);
}
@PostMapping("/tutorials/{tutorialId}/tags")
public ResponseEntity<Tag> addTagToTutorial(@PathVariable(value = "tutorialId") Long tutorialId, @RequestBody Tag tag) {
tutorialManager.addTagToTutorial(tutorialId, tag);
return ResponseEntity.ok().build();
}
@PutMapping("/tags/{tagId}")
public ResponseEntity<Tag> updateTag(@PathVariable("tagId") Long tagId, @RequestBody Tag newTag) {
return ResponseEntity.ok(tagService.update(tagId, newTag));
}
@DeleteMapping("/tutorials/{tutorialId}/tags/{tagId}")
public ResponseEntity<HttpStatus> deleteTagFromTutorial(
@PathVariable(value = "tutorialId") Long tutorialId,
@PathVariable(value = "tagId") Long tagId
) {
tutorialService.deleteTagFromTutorial(tutorialId, tagId);
return ResponseEntity.ok().build();
}
@DeleteMapping("/tags/{tagId}")
public ResponseEntity<HttpStatus> deleteTag(@PathVariable("tagId") Long tagId) {
tagService.deleteById(tagId);
return ResponseEntity.ok().build();
}
}

View File

@ -0,0 +1,98 @@
package dev.struchkov.example.portal.controller;
import dev.struchkov.example.portal.model.Tutorial;
import dev.struchkov.example.portal.service.TutorialService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
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 java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
@CrossOrigin(origins = "http://localhost:8081")
public class TutorialController {
private final TutorialService tutorialService;
@GetMapping("/tutorials")
public ResponseEntity<List<Tutorial>> getAllTutorials(
@RequestParam(required = false) String title
) {
final List<Tutorial> tutorials;
if (title == null) {
tutorials = tutorialService.getAll();
} else {
tutorials = tutorialService.findByTitle(title);
}
if (tutorials.isEmpty()) {
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
return ResponseEntity.ok(tutorials);
}
@GetMapping("/tutorials/{tutorialId}")
public ResponseEntity<Tutorial> getTutorialById(
@PathVariable("tutorialId") Long tutorialId
) {
return ResponseEntity.ok(tutorialService.getByIdOrThrow(tutorialId));
}
@PostMapping("/tutorials")
public ResponseEntity<Tutorial> createTutorial(
@RequestBody Tutorial tutorial
) {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(tutorialService.create(tutorial));
}
@PutMapping("/tutorials/{tutorialId}")
public ResponseEntity<Tutorial> updateTutorial(
@PathVariable("tutorialId") Long tutorialId,
@RequestBody Tutorial newTutorial
) {
return ResponseEntity
.status(HttpStatus.OK)
.body(tutorialService.update(tutorialId, newTutorial));
}
@DeleteMapping("/tutorials/{tutorialId}")
public ResponseEntity<HttpStatus> deleteTutorial(
@PathVariable("tutorialId") Long tutorialId
) {
tutorialService.deleteById(tutorialId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
@DeleteMapping("/tutorials")
public ResponseEntity<HttpStatus> deleteAllTutorials() {
tutorialService.deleteAll();
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
@GetMapping("/tutorials/published")
public ResponseEntity<List<Tutorial>> findByPublished() {
final List<Tutorial> tutorials = tutorialService.findByPublished(true);
if (tutorials.isEmpty()) {
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
return ResponseEntity.ok(tutorials);
}
}

View File

@ -0,0 +1,40 @@
package dev.struchkov.example.portal.exception;
import dev.struchkov.example.portal.model.ErrorMessage;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import java.util.Date;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.NOT_FOUND;
@RestControllerAdvice
public class ControllerExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorMessage> resourceNotFoundException(NotFoundException ex, WebRequest request) {
final ErrorMessage message = new ErrorMessage(
NOT_FOUND.value(),
new Date(),
ex.getMessage(),
request.getDescription(false)
);
return ResponseEntity.status(NOT_FOUND).body(message);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorMessage> globalExceptionHandler(Exception ex, WebRequest request) {
ErrorMessage message = new ErrorMessage(
INTERNAL_SERVER_ERROR.value(),
new Date(),
ex.getMessage(),
request.getDescription(false)
);
return ResponseEntity.status(INTERNAL_SERVER_ERROR).body(message);
}
}

View File

@ -0,0 +1,9 @@
package dev.struchkov.example.portal.exception;
public class NotFoundException extends RuntimeException {
public NotFoundException(String msg) {
super(msg);
}
}

View File

@ -0,0 +1,44 @@
package dev.struchkov.example.portal.manager;
import dev.struchkov.example.portal.model.Comment;
import dev.struchkov.example.portal.model.Tag;
import dev.struchkov.example.portal.model.Tutorial;
import dev.struchkov.example.portal.service.CommentService;
import dev.struchkov.example.portal.service.TagService;
import dev.struchkov.example.portal.service.TutorialService;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class TutorialManager {
private final TutorialService tutorialService;
private final CommentService commentService;
private final TagService tagService;
@Transactional
public void addTagToTutorial(Long tutorialId, Tag tag) {
final Tutorial tutorial = tutorialService.getByIdOrThrow(tutorialId);
final Tag newTag = getOrCreateTag(tag);
tutorial.addTag(newTag);
}
@Transactional
public Comment addNewComment(@NonNull Long tutorialId, @NonNull Comment newComment) {
final Tutorial tutorial = tutorialService.getByIdOrThrow(tutorialId);
newComment.setTutorial(tutorial);
return commentService.create(newComment);
}
private Tag getOrCreateTag(Tag newTag) {
if (newTag.getId() != null) {
return tagService.getByIdOrThrow(newTag.getId());
} else {
return tagService.create(newTag);
}
}
}

View File

@ -0,0 +1,36 @@
package dev.struchkov.example.portal.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Getter
@Setter
@Table(name = "comments")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "comment_generator")
private Long id;
@Column(name = "content")
private String content;
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "tutorial_id", nullable = false)
// @OnDelete(action = OnDeleteAction.CASCADE)
private Tutorial tutorial;
}

View File

@ -0,0 +1,35 @@
package dev.struchkov.example.portal.model;
import java.util.Date;
public class ErrorMessage {
private int statusCode;
private Date timestamp;
private String message;
private String description;
public ErrorMessage(int statusCode, Date timestamp, String message, String description) {
this.statusCode = statusCode;
this.timestamp = timestamp;
this.message = message;
this.description = description;
}
public int getStatusCode() {
return statusCode;
}
public Date getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public String getDescription() {
return description;
}
}

View File

@ -0,0 +1,35 @@
package dev.struchkov.example.portal.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import java.util.HashSet;
import java.util.Set;
@Entity
@Getter
@Setter
@Table(name = "tags")
public class Tag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@JsonIgnore
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "tags")
private Set<Tutorial> tutorials = new HashSet<>();
}

View File

@ -0,0 +1,77 @@
package dev.struchkov.example.portal.model;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import java.util.HashSet;
import java.util.Set;
@Getter
@Setter
@Entity
@NoArgsConstructor
@Table(name = "tutorials")
public class Tutorial {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "tutorial_generator"
)
private Long id;
@Column(name = "title")
private String title;
@Column(name = "description")
private String description;
@Column(name = "published")
private boolean published;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
@JoinColumn(name = "tutorial_id")
private Set<Comment> comments = new HashSet<>();
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(
name = "tutorial_tags",
joinColumns = @JoinColumn(name = "tutorial_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private Set<Tag> tags = new HashSet<>();
public void addTag(Tag tag) {
this.tags.add(tag);
tag.getTutorials().add(this);
}
public void removeTag(Long tagId) {
tags.stream()
.filter(tag -> tag.getId().equals(tagId))
.findFirst()
.ifPresent(foundTag -> {
foundTag.getTutorials().remove(this);
tags.remove(foundTag);
});
}
@Override
public String toString() {
return "Tutorial [id=" + id + ", title=" + title + ", desc=" + description + ", published=" + published + "]";
}
}

View File

@ -0,0 +1,14 @@
package dev.struchkov.example.portal.repository;
import dev.struchkov.example.portal.model.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByTutorialId(Long postId);
void deleteByTutorialId(Long tutorialId);
}

View File

@ -1,6 +1,6 @@
package com.yprac.spring.hibernate.onetomany.repository;
package dev.struchkov.example.portal.repository;
import com.yprac.spring.hibernate.onetomany.model.Tag;
import dev.struchkov.example.portal.model.Tag;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
@ -8,4 +8,5 @@ import java.util.List;
public interface TagRepository extends JpaRepository<Tag, Long> {
List<Tag> findTagsByTutorialsId(Long tutorialId);
}

View File

@ -0,0 +1,16 @@
package dev.struchkov.example.portal.repository;
import dev.struchkov.example.portal.model.Tutorial;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface TutorialRepository extends JpaRepository<Tutorial, Long> {
List<Tutorial> findByPublished(boolean published);
List<Tutorial> findByTitleContaining(String title);
List<Tutorial> findTutorialsByTagsId(Long tagId);
}

View File

@ -0,0 +1,49 @@
package dev.struchkov.example.portal.service;
import dev.struchkov.example.portal.exception.NotFoundException;
import dev.struchkov.example.portal.model.Comment;
import dev.struchkov.example.portal.repository.CommentRepository;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class CommentService {
private final CommentRepository repository;
public Comment create(@NonNull Comment newComment) {
newComment.setId(null);
return repository.save(newComment);
}
public Comment update(@NonNull Long commentId, @NonNull Comment updateComment) {
final Comment comment = repository.findById(commentId)
.orElseThrow(() -> new NotFoundException("Not found Comment with id = " + commentId));
comment.setContent(updateComment.getContent());
return comment;
}
public List<Comment> getByTutorialId(@NonNull Long tutorialId) {
return repository.findByTutorialId(tutorialId);
}
public Comment getByIdOrThrow(@NonNull Long commentId) {
return repository.findById(commentId)
.orElseThrow(() -> new NotFoundException("Not found Comment with id = " + commentId));
}
public void deleteById(@NonNull Long commentId) {
repository.deleteById(commentId);
}
public void deleteAllByTutorialId(@NonNull Long tutorialId) {
repository.deleteByTutorialId(tutorialId);
}
}

View File

@ -0,0 +1,48 @@
package dev.struchkov.example.portal.service;
import dev.struchkov.example.portal.exception.NotFoundException;
import dev.struchkov.example.portal.model.Tag;
import dev.struchkov.example.portal.repository.TagRepository;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TagService {
private final TagRepository repository;
public Tag create(@NonNull Tag newTag) {
return repository.save(newTag);
}
public Tag update(@NonNull Long tagId, @NonNull Tag newTag) {
final Tag oldTag = repository.findById(tagId)
.orElseThrow(() -> new NotFoundException("Not found Tag with id = " + tagId));
oldTag.setName(newTag.getName());
return oldTag;
}
public List<Tag> getAll() {
return repository.findAll();
}
public List<Tag> getAllByTutorialId(@NonNull Long tutorialId) {
return repository.findTagsByTutorialsId(tutorialId);
}
public Tag getByIdOrThrow(@NonNull Long tagId) {
return repository.findById(tagId)
.orElseThrow(() -> new NotFoundException("Not found Tag with id = " + tagId));
}
public void deleteById(@NonNull Long tagId) {
repository.deleteById(tagId);
}
}

View File

@ -0,0 +1,69 @@
package dev.struchkov.example.portal.service;
import dev.struchkov.example.portal.exception.NotFoundException;
import dev.struchkov.example.portal.model.Tutorial;
import dev.struchkov.example.portal.repository.TutorialRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TutorialService {
private final TutorialRepository repository;
public Tutorial getByIdOrThrow(Long tutorialId) {
return repository.findById(tutorialId)
.orElseThrow(() -> new NotFoundException("Not found Tutorial with id = " + tutorialId));
}
public List<Tutorial> getAll() {
return repository.findAll();
}
public List<Tutorial> findByTitle(String title) {
return repository.findByTitleContaining(title);
}
public Tutorial create(Tutorial tutorial) {
tutorial.setId(null);
tutorial.setPublished(true);
return repository.save(tutorial);
}
public Tutorial update(Long tutorialId, Tutorial newTutorial) {
final Tutorial oldTutorial = repository.findById(tutorialId)
.orElseThrow(() -> new NotFoundException("Not found Tutorial with id = " + tutorialId));
oldTutorial.setDescription(newTutorial.getDescription());
oldTutorial.setTitle(newTutorial.getTitle());
oldTutorial.setPublished(newTutorial.isPublished());
return oldTutorial;
}
public void deleteById(Long tutorialId) {
repository.deleteById(tutorialId);
}
public void deleteAll() {
repository.deleteAll();
}
public List<Tutorial> findByPublished(boolean published) {
return repository.findByPublished(published);
}
public List<Tutorial> getAllByTagId(Long tagId) {
return repository.findTutorialsByTagsId(tagId);
}
public void deleteTagFromTutorial(Long tutorialId, Long tagId) {
repository.findById(tutorialId)
.orElseThrow(() -> new NotFoundException("Not found Tutorial with id = " + tutorialId))
.removeTag(tagId);
}
}

View File

@ -1,11 +0,0 @@
spring.datasource.url=
spring.datasource.username=
spring.datasource.password=
spring.jpa.show-sql=true
#spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.PostgreSQL9Dialect
#spring.jpa.generate.ddl=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true

View File

@ -0,0 +1,16 @@
spring:
datasource:
url: jdbc:postgresql://localhost/test
username: postgres
password: 121314Ma
driver-class-name: org.postgresql.Driver
jpa:
show-sql: true
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQL10Dialect
jdbc:
lob:
non_contextual_creation: true

View File

@ -1,13 +0,0 @@
package com.yprac.spring.hibernate.onetomany;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringBootOneToManyApplicationTests {
@Test
void contextLoads() {
}
}