diff --git a/.gitignore b/.gitignore index c2065bc..fa56d76 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +application.yml +java.docker.day.2-0.0.1-SNAPSHOT.jar \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ea842b5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM eclipse-temurin:18 + +WORKDIR /app + +COPY java.docker.day.2-0.0.1.jar /app/java.docker.day.2-0.0.1.jar + +EXPOSE 4000 + +ENTRYPOINT ["java", "-jar", "java.docker.day.2-0.0.1.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index fb070c4..0efd7da 100644 --- a/build.gradle +++ b/build.gradle @@ -31,3 +31,14 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + +jar { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + manifest { + attributes 'Main-Class': 'com.booleanuk.api.Main' + } + + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4b6aa6f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +services: + app: + image: 'music-box:latest' + container_name: app + depends_on: + - db + ports: + - '4000:4000' + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mypostgresuser + - SPRING_DATASOURCE_USERNAME=mypostgresuser + - SPRING_DATASOURCE_PASSWORD=mypostgrespassword + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + + db: + image: 'postgres:latest' + container_name: db + environment: + - POSTGRES_USER=mypostgresuser + - POSTGRES_DATABASE=mypostgresuser + - POSTGRES_PASSWORD=mypostgrespassword \ No newline at end of file diff --git a/src/main/java/com/booleanuk/api/Main.java b/src/main/java/com/booleanuk/api/Main.java new file mode 100644 index 0000000..8e749e0 --- /dev/null +++ b/src/main/java/com/booleanuk/api/Main.java @@ -0,0 +1,11 @@ +package com.booleanuk.api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} diff --git a/src/main/java/com/booleanuk/api/controllers/UserController.java b/src/main/java/com/booleanuk/api/controllers/UserController.java new file mode 100644 index 0000000..70286ba --- /dev/null +++ b/src/main/java/com/booleanuk/api/controllers/UserController.java @@ -0,0 +1,202 @@ +package com.booleanuk.api.controllers; + +import com.booleanuk.api.models.BlogPost; +import com.booleanuk.api.models.Comment; +import com.booleanuk.api.models.User; +import com.booleanuk.api.repositories.BlogPostRepository; +import com.booleanuk.api.repositories.CommentRepository; +import com.booleanuk.api.repositories.UserRepository; +import com.booleanuk.api.responses.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("users") +public class UserController { + + @Autowired + private UserRepository userRepository; + + @Autowired + private BlogPostRepository blogPostRepository; + + @Autowired + private CommentRepository commentRepository; + + // Create user + @PostMapping + public ResponseEntity create(@RequestBody User user) { + this.userRepository.save(user); + UserResponse response = new UserResponse(); + response.set(user); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + // Get all users + @GetMapping + public ResponseEntity getAll() { + UserListResponse response = new UserListResponse(); + response.set(this.userRepository.findAll()); + return ResponseEntity.ok(response); + } + + // Get specific user + @GetMapping("{id}") + public ResponseEntity> getSpecific(@PathVariable int id) { + User user = this.userRepository.findById(id).orElse(null); + if (user == null) { + ErrorResponse error = new ErrorResponse(); + error.set("A user with this id was not found."); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + UserResponse response = new UserResponse(); + response.set(user); + return ResponseEntity.ok(response); + } + + // Update user + @PutMapping("{id}") + public ResponseEntity> update(@PathVariable int id, @RequestBody User user) { + User originalUser = this.userRepository.findById(id).orElse(null); + if (originalUser == null) { + ErrorResponse error = new ErrorResponse(); + error.set("Not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + user.setId(id); + try { + this.userRepository.save(user); + } catch (Exception e) { + ErrorResponse error = new ErrorResponse(); + error.set("Bad request"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + UserResponse response = new UserResponse(); + response.set(user); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + // Delete user + @DeleteMapping("{id}") + public ResponseEntity> delete(@PathVariable int id) { + User user = this.userRepository.findById(id).orElse(null); + if (user == null) { + ErrorResponse error = new ErrorResponse(); + error.set("Not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + UserResponse response = new UserResponse(); + response.set(user); + + try { + this.userRepository.delete(user); + } catch (Exception e) { + ErrorResponse error = new ErrorResponse(); + error.set("Bad request"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + return ResponseEntity.ok(response); + } + + // Post blogpost for specific user + @PostMapping("{userId}/blogposts") + public ResponseEntity> publishBlogPostForUser( + @PathVariable int userId, + @RequestBody BlogPost blogpost) { + + try { + User user = this.userRepository.findById(userId).orElse(null); + if (user == null) { + ErrorResponse error = new ErrorResponse(); + error.set("A user with this id was not found."); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + // Populate blogpost with publisher info and save + blogpost.setPublisher(user); + this.blogPostRepository.save(blogpost); + + // Add blogpost to user and save user + user.getBlogPosts().add(blogpost); + this.userRepository.save(user); + + BlogPostResponse response = new BlogPostResponse(); + response.set(blogpost); + return new ResponseEntity<>(response, HttpStatus.CREATED); + + } catch (Exception e) { + ErrorResponse error = new ErrorResponse(); + error.set("An error occurred: " + e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + // Post comment on blogpost + // NOTE: simplified, could/should be done in other ways + @PostMapping("{userId}/blogposts/{blogpostId}/comment") + public ResponseEntity> publishCommentOnBlogPostForUser( + @PathVariable int userId, + @PathVariable int blogpostId, + @RequestBody Comment comment) { + + try { + // Find corresponding user (publisher) + User user = this.userRepository.findById(userId).orElse(null); + if (user == null) { + ErrorResponse error = new ErrorResponse(); + error.set("A user with this id was not found."); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + comment.setPublisher(user); + + // Find corresponding blogpost + BlogPost blogpost = this.blogPostRepository.findById(blogpostId).orElse(null); + if (blogpost == null) { + ErrorResponse error = new ErrorResponse(); + error.set("A blogpost with this id was not found."); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + blogpost.getComments().add(comment); + comment.setBlogPost(blogpost); + + this.commentRepository.save(comment); + + CommentResponse response = new CommentResponse(); + response.set(comment); + return new ResponseEntity<>(response, HttpStatus.CREATED); + + } catch (Exception e) { + ErrorResponse error = new ErrorResponse(); + error.set("An error occurred: " + e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("blogposts") + public ResponseEntity getAllBlogposts() { + BlogPostListResponse response = new BlogPostListResponse(); + response.set(this.blogPostRepository.findAll()); + return ResponseEntity.ok(response); + } + + // Get all comments for a specific blogpost + @GetMapping("blogposts/{blogpostId}/comments") + public ResponseEntity> getCommentOnBlogPostForUser( + @PathVariable int blogpostId) { + + BlogPost blogpost = this.blogPostRepository.findById(blogpostId).orElse(null); + if (blogpost == null) { + ErrorResponse error = new ErrorResponse(); + error.set("A blogpost with this id was not found."); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + BlogPostResponse response = new BlogPostResponse(); + response.set(blogpost); + return ResponseEntity.ok(response); + } + +} diff --git a/src/main/java/com/booleanuk/api/models/BlogPost.java b/src/main/java/com/booleanuk/api/models/BlogPost.java new file mode 100644 index 0000000..84ee949 --- /dev/null +++ b/src/main/java/com/booleanuk/api/models/BlogPost.java @@ -0,0 +1,43 @@ +package com.booleanuk.api.models; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +@Entity +@Table +public class BlogPost { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @ManyToOne + @JoinColumn + @JsonBackReference // To avoid recursion error + private User publisher; + + @CreationTimestamp + @Column(updatable = false) + private LocalDateTime createdAt; + + // Content text of blogpost + @Column + private String text; + + @OneToMany + @JoinColumn + private List comments = new ArrayList<>(); + + public BlogPost(String text) { + this.text = text; + } +} diff --git a/src/main/java/com/booleanuk/api/models/Comment.java b/src/main/java/com/booleanuk/api/models/Comment.java new file mode 100644 index 0000000..14f9e94 --- /dev/null +++ b/src/main/java/com/booleanuk/api/models/Comment.java @@ -0,0 +1,48 @@ +package com.booleanuk.api.models; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@Entity +@Table +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @ManyToOne // Many comments can be published by one user + @JoinColumn + @JsonIgnoreProperties({"comment"}) + private User publisher; + + @ManyToOne // Many comments can belong to one blog post + @JoinColumn + @JsonBackReference // To avoid recursion error + private BlogPost blogPost; + + @CreationTimestamp + @Column(updatable = false) + private LocalDateTime createdAt; + + @Column + private String text; + + public Comment(User publisher, BlogPost blogPost, String text) { + this.publisher = publisher; + this.blogPost = blogPost; + this.text = text; + } + + public Comment(String text) { + this.text = text; + } +} diff --git a/src/main/java/com/booleanuk/api/models/User.java b/src/main/java/com/booleanuk/api/models/User.java new file mode 100644 index 0000000..537498c --- /dev/null +++ b/src/main/java/com/booleanuk/api/models/User.java @@ -0,0 +1,37 @@ +package com.booleanuk.api.models; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +@Entity(name = "users") +@Table +@JsonIgnoreProperties({"blogPosts"}) +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column + private String name; + + @Column + private String username; + + @OneToMany(mappedBy = "publisher", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference // To avoid recursion error + private List blogPosts = new ArrayList<>(); + + public User(String name, String username) { + this.name = name; + this.username = username; + } +} diff --git a/src/main/java/com/booleanuk/api/repositories/BlogPostRepository.java b/src/main/java/com/booleanuk/api/repositories/BlogPostRepository.java new file mode 100644 index 0000000..7d98795 --- /dev/null +++ b/src/main/java/com/booleanuk/api/repositories/BlogPostRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.api.repositories; + +import com.booleanuk.api.models.BlogPost; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BlogPostRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/api/repositories/CommentRepository.java b/src/main/java/com/booleanuk/api/repositories/CommentRepository.java new file mode 100644 index 0000000..356354d --- /dev/null +++ b/src/main/java/com/booleanuk/api/repositories/CommentRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.api.repositories; + +import com.booleanuk.api.models.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/api/repositories/UserRepository.java b/src/main/java/com/booleanuk/api/repositories/UserRepository.java new file mode 100644 index 0000000..26097e0 --- /dev/null +++ b/src/main/java/com/booleanuk/api/repositories/UserRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.api.repositories; + +import com.booleanuk.api.models.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/api/responses/BlogPostListResponse.java b/src/main/java/com/booleanuk/api/responses/BlogPostListResponse.java new file mode 100644 index 0000000..1674c16 --- /dev/null +++ b/src/main/java/com/booleanuk/api/responses/BlogPostListResponse.java @@ -0,0 +1,8 @@ +package com.booleanuk.api.responses; + +import com.booleanuk.api.models.BlogPost; + +import java.util.List; + +public class BlogPostListResponse extends Response> { +} diff --git a/src/main/java/com/booleanuk/api/responses/BlogPostResponse.java b/src/main/java/com/booleanuk/api/responses/BlogPostResponse.java new file mode 100644 index 0000000..f796c6c --- /dev/null +++ b/src/main/java/com/booleanuk/api/responses/BlogPostResponse.java @@ -0,0 +1,6 @@ +package com.booleanuk.api.responses; + +import com.booleanuk.api.models.BlogPost; + +public class BlogPostResponse extends Response { +} diff --git a/src/main/java/com/booleanuk/api/responses/CommentResponse.java b/src/main/java/com/booleanuk/api/responses/CommentResponse.java new file mode 100644 index 0000000..0d7dccc --- /dev/null +++ b/src/main/java/com/booleanuk/api/responses/CommentResponse.java @@ -0,0 +1,6 @@ +package com.booleanuk.api.responses; + +import com.booleanuk.api.models.Comment; + +public class CommentResponse extends Response { +} diff --git a/src/main/java/com/booleanuk/api/responses/ErrorResponse.java b/src/main/java/com/booleanuk/api/responses/ErrorResponse.java new file mode 100644 index 0000000..bda3184 --- /dev/null +++ b/src/main/java/com/booleanuk/api/responses/ErrorResponse.java @@ -0,0 +1,19 @@ +package com.booleanuk.api.responses; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +@Getter +@NoArgsConstructor +public class ErrorResponse extends Response> { + public void set(String message) { + this.status = "error"; + + Map reply = new HashMap<>(); + reply.put("message", message); + this.data = reply; + } +} diff --git a/src/main/java/com/booleanuk/api/responses/Response.java b/src/main/java/com/booleanuk/api/responses/Response.java new file mode 100644 index 0000000..e7fa6f3 --- /dev/null +++ b/src/main/java/com/booleanuk/api/responses/Response.java @@ -0,0 +1,14 @@ +package com.booleanuk.api.responses; + +import lombok.Getter; + +@Getter +public class Response { + protected String status; + protected T data; + + public void set(T data) { + this.status = "success"; + this.data = data; + } +} diff --git a/src/main/java/com/booleanuk/api/responses/UserListResponse.java b/src/main/java/com/booleanuk/api/responses/UserListResponse.java new file mode 100644 index 0000000..834dac5 --- /dev/null +++ b/src/main/java/com/booleanuk/api/responses/UserListResponse.java @@ -0,0 +1,8 @@ +package com.booleanuk.api.responses; + +import com.booleanuk.api.models.User; + +import java.util.List; + +public class UserListResponse extends Response>{ +} diff --git a/src/main/java/com/booleanuk/api/responses/UserResponse.java b/src/main/java/com/booleanuk/api/responses/UserResponse.java new file mode 100644 index 0000000..4ed5485 --- /dev/null +++ b/src/main/java/com/booleanuk/api/responses/UserResponse.java @@ -0,0 +1,6 @@ +package com.booleanuk.api.responses; + +import com.booleanuk.api.models.User; + +public class UserResponse extends Response{ +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index 02c2591..0000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,7 +0,0 @@ -server: - port: 4000 - error: - include-message: always - include-binding-errors: always - include-stacktrace: never - include-exception: false