Skip to content
Open

ok #55

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Empty file.
Binary file not shown.
2 changes: 2 additions & 0 deletions java/socialapp/.gradle/buildOutputCleanup/cache.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#Thu Jan 29 22:05:10 UTC 2026
gradle.version=9.2.1
Binary file not shown.
Empty file.
38 changes: 38 additions & 0 deletions java/socialapp/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.0'
}

group = 'com.contoso'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}

repositories {
mavenCentral()
}

dependencies {
implementation platform('org.springframework.boot:spring-boot-dependencies:3.2.0')
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'

implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2'
implementation 'org.xerial:sqlite-jdbc:3.42.0.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.2'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}

bootJar {
launchScript()
}
68 changes: 68 additions & 0 deletions java/socialapp/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.contoso</groupId>
<artifactId>socialapp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<java.version>21</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.42.0.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1 change: 1 addition & 0 deletions java/socialapp/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'socialapp'
Binary file added java/socialapp/sns_api.db
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.contoso.socialapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.contoso.socialapp.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import jakarta.annotation.PostConstruct;

@Component
public class DatabaseInitializer {

private final JdbcTemplate jdbc;

@Autowired
public DatabaseInitializer(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}

@PostConstruct
public void init() {
// Drop and recreate tables on every startup to mirror FastAPI init_database behavior
jdbc.execute("DROP TABLE IF EXISTS likes");
jdbc.execute("DROP TABLE IF EXISTS comments");
jdbc.execute("DROP TABLE IF EXISTS posts");

jdbc.execute("CREATE TABLE posts ("
+ "id TEXT PRIMARY KEY,"
+ "username TEXT NOT NULL,"
+ "content TEXT NOT NULL,"
+ "created_at TEXT NOT NULL,"
+ "updated_at TEXT NOT NULL,"
+ "likes INTEGER NOT NULL,"
+ "likes_by TEXT NOT NULL"
+ ")");

jdbc.execute("CREATE TABLE comments ("
+ "id TEXT PRIMARY KEY,"
+ "post_id TEXT NOT NULL,"
+ "username TEXT NOT NULL,"
+ "content TEXT NOT NULL,"
+ "created_at TEXT NOT NULL,"
+ "updated_at TEXT NOT NULL,"
+ "likes INTEGER NOT NULL"
+ ")");

jdbc.execute("CREATE TABLE likes ("
+ "like_id TEXT PRIMARY KEY,"
+ "post_id TEXT NOT NULL,"
+ "username TEXT NOT NULL"
+ ")");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.contoso.socialapp.controller;

import org.springframework.web.bind.annotation.RestController;

// Note: Comments and comment endpoints are handled within PostController to match path structure from the OpenAPI contract.
@RestController
public class CommentController {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.contoso.socialapp.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class HealthController {

@GetMapping("/health")
public ResponseEntity<Map<String, String>> health() {
return ResponseEntity.ok(Map.of("status", "healthy", "message", "API is running successfully"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.contoso.socialapp.controller;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.InputStream;

@RestController
public class OpenApiController {

@GetMapping(value = "/v3/api-docs", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<JsonNode> apiDocs() throws Exception {
ClassPathResource r = new ClassPathResource("static/openapi.yaml");
try (InputStream in = r.getInputStream()) {
ObjectMapper yamlReader = new YAMLMapper();
JsonNode obj = yamlReader.readTree(in);
return ResponseEntity.ok(obj);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.contoso.socialapp.controller;

import com.contoso.socialapp.model.dto.*;
import com.contoso.socialapp.service.CommentService;
import com.contoso.socialapp.service.LikeService;
import com.contoso.socialapp.service.PostService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api")
@Validated
public class PostController {
private final PostService postService;
private final CommentService commentService;
private final LikeService likeService;

@Autowired
public PostController(PostService postService, CommentService commentService, LikeService likeService) {
this.postService = postService;
this.commentService = commentService;
this.likeService = likeService;
}

@GetMapping("/posts")
public ResponseEntity<List<PostDTO>> listPosts() {
return ResponseEntity.ok(postService.listPosts());
}

@PostMapping("/posts")
public ResponseEntity<PostDTO> createPost(@Valid @RequestBody NewPostRequest req) {
PostDTO p = postService.createPost(req.getUsername(), req.getContent());
return ResponseEntity.status(HttpStatus.CREATED).body(p);
}

@GetMapping("/posts/{postId}")
public ResponseEntity<PostDTO> getPost(@PathVariable("postId") String postId) {
Optional<PostDTO> p = postService.getPostById(postId);
if (p.isEmpty()) throw new com.contoso.socialapp.exception.ResourceNotFoundException("Post with ID '" + postId + "' not found");
return ResponseEntity.ok(p.get());
}

@PatchMapping("/posts/{postId}")
public ResponseEntity<PostDTO> updatePost(@PathVariable("postId") String postId, @Valid @RequestBody UpdatePostRequest req) {
Optional<PostDTO> p = postService.updatePost(postId, req.getUsername(), req.getContent());
if (p.isEmpty()) throw new com.contoso.socialapp.exception.ResourceNotFoundException("Post not found or you do not have permission to update it");
return ResponseEntity.ok(p.get());
}

@DeleteMapping("/posts/{postId}")
public ResponseEntity<Void> deletePost(@PathVariable("postId") String postId) {
boolean deleted = postService.deletePost(postId);
if (!deleted) throw new com.contoso.socialapp.exception.ResourceNotFoundException("Post with ID '" + postId + "' not found");
return ResponseEntity.noContent().build();
}

// Comments
@GetMapping("/posts/{postId}/comments")
public ResponseEntity<List<CommentDTO>> listComments(@PathVariable("postId") String postId) {
// Verify post exists
Optional<PostDTO> p = postService.getPostById(postId);
if (p.isEmpty()) throw new com.contoso.socialapp.exception.ResourceNotFoundException("Post with ID '" + postId + "' not found");
return ResponseEntity.ok(commentService.listCommentsByPostId(postId));
}

@PostMapping("/posts/{postId}/comments")
public ResponseEntity<CommentDTO> createComment(@PathVariable("postId") String postId, @Valid @RequestBody NewCommentRequest req) {
var c = commentService.createComment(postId, req.getUsername(), req.getContent());
if (c.isEmpty()) throw new com.contoso.socialapp.exception.ResourceNotFoundException("Post with ID '" + postId + "' not found");
return ResponseEntity.status(HttpStatus.CREATED).body(c.get());
}

@GetMapping("/posts/{postId}/comments/{commentId}")
public ResponseEntity<CommentDTO> getComment(@PathVariable("postId") String postId, @PathVariable("commentId") String commentId) {
var c = commentService.getComment(postId, commentId);
if (c.isEmpty()) throw new com.contoso.socialapp.exception.ResourceNotFoundException("Comment with ID '" + commentId + "' not found on post '" + postId + "'");
return ResponseEntity.ok(c.get());
}

@PatchMapping("/posts/{postId}/comments/{commentId}")
public ResponseEntity<CommentDTO> updateComment(@PathVariable("postId") String postId, @PathVariable("commentId") String commentId, @Valid @RequestBody UpdateCommentRequest req) {
var c = commentService.updateComment(postId, commentId, req.getUsername(), req.getContent());
if (c.isEmpty()) throw new com.contoso.socialapp.exception.ResourceNotFoundException("Comment not found or you do not have permission to update it");
return ResponseEntity.ok(c.get());
}

@DeleteMapping("/posts/{postId}/comments/{commentId}")
public ResponseEntity<Void> deleteComment(@PathVariable("postId") String postId, @PathVariable("commentId") String commentId) {
boolean deleted = commentService.deleteComment(postId, commentId);
if (!deleted) throw new com.contoso.socialapp.exception.ResourceNotFoundException("Comment with ID '" + commentId + "' not found");
return ResponseEntity.noContent().build();
}

// Likes
@PostMapping("/posts/{postId}/likes")
public ResponseEntity<LikeResponse> likePost(@PathVariable("postId") String postId, @Valid @RequestBody LikeRequest req) {
var r = likeService.addLike(postId, req.getUsername());
if (r == null) throw new com.contoso.socialapp.exception.ResourceNotFoundException("Post with ID '" + postId + "' not found");
return ResponseEntity.status(HttpStatus.CREATED).body(r);
}

@DeleteMapping("/posts/{postId}/likes")
public ResponseEntity<Void> unlikePost(@PathVariable("postId") String postId, @Valid @RequestBody LikeRequest req) {
boolean existsPost = postService.getPostById(postId).isPresent();
if (!existsPost) throw new com.contoso.socialapp.exception.ResourceNotFoundException("Post with ID '" + postId + "' not found");
likeService.removeLike(postId, req.getUsername());
return ResponseEntity.noContent().build();
}
}
Loading