diff --git a/Dockerfile b/Dockerfile index 6bff3d3..f5f1458 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,11 @@ -FROM eclipse-temurin:21-jre-alpine -WORKDIR /app -# Копируем готовый jar из папки build/libs -COPY build/libs/*.jar app.jar -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file +# Этап 1 — сборка jar внутри Docker + FROM gradle:8-jdk21-alpine AS builder + WORKDIR /app + COPY . . + RUN gradle build -x test + + # Этап 2 — запуск + FROM eclipse-temurin:21-jre-alpine + WORKDIR /app + COPY --from=builder /app/build/libs/*.jar app.jar + ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8eaebe0..676d055 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,11 @@ group = 'com.codzilla' version = '0.0.1' description = 'Backend' +jar { + + enabled = false +} + java { toolchain { languageVersion = JavaLanguageVersion.of(21) @@ -21,9 +26,14 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.springframework.boot:spring-boot-starter-websocket' developmentOnly 'org.springframework.boot:spring-boot-devtools' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + runtimeOnly 'org.postgresql:postgresql' testRuntimeOnly 'com.h2database:h2' diff --git a/src/main/java/com/codzilla/backend/controller/Coffee.java b/src/main/java/com/codzilla/backend/controller/Coffee.java deleted file mode 100644 index 01c16ba..0000000 --- a/src/main/java/com/codzilla/backend/controller/Coffee.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.codzilla.backend.controller; - - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; - -import java.util.UUID; - -@Entity -public class Coffee { - - - @Id - private String id ; - private String name; - public void setId(String id) { - this.id = id; - } - - - @JsonCreator - public Coffee(@JsonProperty("id") String id, @JsonProperty("name") String name) { - this.id = id; - this.name = name; - } - - public Coffee() { - this.id = UUID.randomUUID().toString(); - this.name = "DEFAULT"; - - } - - public Coffee(String name) { - this(UUID.randomUUID().toString() , name) ; - } - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/com/codzilla/backend/controller/DataLoader.java b/src/main/java/com/codzilla/backend/controller/DataLoader.java deleted file mode 100644 index dad4e90..0000000 --- a/src/main/java/com/codzilla/backend/controller/DataLoader.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.codzilla.backend.controller; - -import com.codzilla.backend.repository.CoffeeRepository; -import jakarta.annotation.PostConstruct; -import org.springframework.stereotype.Component; - -import java.util.List; - -@Component -public class DataLoader { - public final CoffeeRepository coffeeRepository; - - public DataLoader(CoffeeRepository coffeeRepository) { - this.coffeeRepository = coffeeRepository; - } - - @PostConstruct - private void loadData() { - coffeeRepository.saveAll(List.of( - new Coffee("Café Cereza"), - new Coffee("Café Ganador"), - new Coffee("Café Lareño"), - new Coffee("Café Três Pontas") - )); - } -} diff --git a/src/main/java/com/codzilla/backend/controller/RestApiDemoController.java b/src/main/java/com/codzilla/backend/controller/RestApiDemoController.java deleted file mode 100644 index e7c43a8..0000000 --- a/src/main/java/com/codzilla/backend/controller/RestApiDemoController.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.codzilla.backend.controller; - -import com.codzilla.backend.repository.CoffeeRepository; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.Optional; - -@RestController -@RequestMapping("/coffee") -public class RestApiDemoController { - - private final CoffeeRepository coffeeRepository; -// private final List coffees = new ArrayList<>(); - - public RestApiDemoController(CoffeeRepository coffeeRepository) { - this.coffeeRepository = coffeeRepository; - - this.coffeeRepository.saveAll(List.of( - new Coffee("Café Cereza"), - new Coffee("Café Ganador"), - new Coffee("Café Lareño"), - new Coffee("Café Três Pontas") - )); - } - - - - @RequestMapping(value = "", method = RequestMethod.GET) - Iterable getCoffees() { - return coffeeRepository.findAll(); - } - - @GetMapping("/{id}") - Optional getCoffee(@PathVariable String id) { - return coffeeRepository.findById(id); - } - - @PostMapping - Coffee postCoffee(@RequestBody Coffee coffee) { - return coffeeRepository.save(coffee); - } - -// @PutMapping("/{id}") -// Coffee putCoffee(@PathVariable String id, @RequestBody Coffee coffee) { -// int coffeeIndex = -1; -// for (Coffee c: coffees) { -// if (c.getId().equals(id)) { -// coffeeIndex = coffees.indexOf(c); -// coffees.set(coffeeIndex, coffee); -// } -// } -// return (coffeeIndex == -1) ? postCoffee(coffee) : coffee; -// } - -// @PutMapping("/{id}") -// ResponseEntity putCoffee(@PathVariable String id, -// @RequestBody Coffee coffee) { -// int coffeeIndex = -1; -// for (Coffee c: coffees) { -// if (c.getId().equals(id)) { -// coffeeIndex = coffees.indexOf(c); -// coffees.set(coffeeIndex, coffee); -// } -// } -// return (coffeeIndex == -1) ? -// new ResponseEntity<>(postCoffee(coffee), HttpStatus.CREATED) : -// new ResponseEntity<>(coffee, HttpStatus.OK); -// } - - @PutMapping("/{id}") - ResponseEntity putCoffee(@PathVariable String id , @RequestBody Coffee coffee){ - return (!coffeeRepository.existsById(id)) ? - new ResponseEntity<>(coffeeRepository.save(coffee) , HttpStatus.CREATED) : - new ResponseEntity<>(coffeeRepository.save(coffee) , HttpStatus.OK); - - } - - @DeleteMapping("/{id}") - void deleteCoffee(@PathVariable String id) { - coffeeRepository.deleteById(id); - } -} diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/judge0/Judge0Client.java b/src/main/java/com/codzilla/backend/controller/Sandbox/judge0/Judge0Client.java new file mode 100644 index 0000000..65ae69e --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/judge0/Judge0Client.java @@ -0,0 +1,75 @@ +package com.codzilla.backend.controller.Sandbox.judge0; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; +import org.springframework.http.MediaType; + +@Slf4j +@Component +public class Judge0Client { + + private final RestClient restClient; + private final ObjectMapper objectMapper = new ObjectMapper(); + + public Judge0Client(@Value("${judge0.base-url}") String baseUrl) { + this.restClient = RestClient.builder() + .baseUrl(baseUrl) + .build(); + } + + public String submit(String sourceCode, int languageId, String stdin) { + try { + String body = objectMapper.writeValueAsString(new SubmissionRequest(sourceCode, languageId, stdin)); + log.info("Sending to Judge0 body: {}", body); + + String raw = restClient.post() + .uri("/submissions?wait=true") + .contentType(MediaType.APPLICATION_JSON) + .body(body) + .retrieve() + .onStatus(status -> true, (req, res) -> {}) + .body(String.class); + + log.info("Judge0 response: {}", raw); + + SubmissionResponse response = objectMapper.readValue(raw, SubmissionResponse.class); + return response.getStatus().getDescription(); + + } catch (Exception e) { + throw new RuntimeException("Judge0 error: " + e.getMessage(), e); + } + } + + public static class SubmissionRequest { + public String source_code; + public Integer language_id; + public String stdin; + + public SubmissionRequest(String sourceCode, int languageId, String stdin) { + this.source_code = sourceCode; + this.language_id = languageId; + this.stdin = stdin; + } + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SubmissionResponse { + private String stdout; + private String stderr; + private String compile_output; + private Status status; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Status { + private int id; + private String description; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/CreateProblemRequest.java b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/CreateProblemRequest.java new file mode 100644 index 0000000..2107354 --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/CreateProblemRequest.java @@ -0,0 +1,19 @@ +package com.codzilla.backend.controller.Sandbox.polygon; + +import com.codzilla.backend.controller.Sandbox.problem.Problem; +import lombok.Data; +import java.util.List; + +@Data +public class CreateProblemRequest { + private String name; // название задачи + private Problem.ProblemType type; + private Problem.ProblemLevel level; + private List tests; // список тестов + + @Data + public static class TestCase { + private String input; // "1 2" + private String output; // "3" + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonClient.java b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonClient.java new file mode 100644 index 0000000..90b4764 --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonClient.java @@ -0,0 +1,171 @@ +package com.codzilla.backend.controller.Sandbox.polygon; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; +import com.fasterxml.jackson.databind.ObjectMapper; + + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.time.Instant; + +import java.util.Random; +import java.util.TreeMap; +import java.util.stream.Collectors; + + + + +@Slf4j +@Component +public class PolygonClient { + + private static final String BASE_URL = "https://polygon.codeforces.com/api/"; + + @Value("${polygon.api.key}") + private String apiKey; + + @Value("${polygon.api.secret}") + private String apiSecret; + + private final RestClient restClient; + + public PolygonClient() { + this.restClient = RestClient.builder() + .baseUrl(BASE_URL) + .defaultHeader("Accept", "application/json") + .defaultHeader("Content-Type", "application/json") + .build(); + } + + + public PolygonProblem getProblemTests(String problemId) { + var params = new TreeMap(); + params.put("problemId", problemId); + params.put("testset", "tests"); + + String url = buildSignedUrl("problem.tests", params); + + String raw = restClient.get() + .uri(url) + .retrieve() + .onStatus(status -> true, (req, res) -> {}) + .body(String.class); + + log.info("getProblemTests response: {}", raw); + + try { + PolygonProblem result = objectMapper.readValue(raw, PolygonProblem.class); + if (!"OK".equals(result.getStatus())) { + throw new RuntimeException("Polygon error getting tests: " + raw); + } + return result; + } catch (Exception e) { + throw new RuntimeException("Failed to parse tests response: " + raw, e); + } + } + + + private String buildSignedUrl(String method, TreeMap params) { + String rand = String.format("%06x", new SecureRandom().nextInt(0xFFFFFF)); + + params.put("apiKey", apiKey); + params.put("time", String.valueOf(Instant.now().getEpochSecond())); + + String paramStr = params.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining("&")); + + String dataToSign = rand + "/" + method + "?" + paramStr + "#" + apiSecret; + log.info("toSign: {}", dataToSign); + + String hash = sha512(dataToSign); + params.put("apiSig", rand + hash); + + + String query = params.entrySet().stream() + .map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8) + + "=" + + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&")); + + return method + "?" + query; + } + + + + private String sha512(String data) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-512"); + byte[] bytes = md.digest(data.getBytes("UTF-8")); // явно UTF-8 + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); // %02x гарантирует ведущий ноль + } + return sb.toString(); + } catch (Exception e) { + throw new RuntimeException("Failed to sign request", e); + } + } + + + + + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public String createProblem(String name) { + var params = new TreeMap(); + params.put("name", name); + + String url = buildSignedUrl("problem.create", params); + + + String raw = restClient.post() + .uri(url) + .retrieve() + .onStatus(status -> true, (req, res) -> {}) // не бросать исключение на 4xx + .body(String.class); + + log.info("Polygon response: {}", raw); + + try { + PolygonResponse response = objectMapper.readValue(raw, PolygonResponse.class); + if (!"OK".equals(response.getStatus())) { + throw new RuntimeException("Polygon error: " + response.getComment()); + } + return response.getResult().getId().toString(); + } catch (Exception e) { + throw new RuntimeException("Failed to parse Polygon response: " + raw, e); + } + } + + + + public void saveTest(String problemId, int index, String input, String output) { + var params = new TreeMap(); + params.put("problemId", problemId); + params.put("testset", "tests"); + params.put("testIndex", String.valueOf(index)); + params.put("testInput", input); + params.put("testOutput", output); + params.put("testUseInStatements", "false"); + + String url = buildSignedUrl("problem.saveTest", params); + + String raw = restClient.post() + .uri(url) + .retrieve() + .onStatus(status -> true, (req, res) -> {}) + .body(String.class); + + log.info("saveTest response: {}", raw); + + } + + +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonProblem.java b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonProblem.java new file mode 100644 index 0000000..1cc0f20 --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonProblem.java @@ -0,0 +1,20 @@ +package com.codzilla.backend.controller.Sandbox.polygon; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import java.util.List; + +@Data + +public class PolygonProblem { + private String status; + private List result; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Test { + private int index; + private String input; + private String output; + } +} diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonResponse.java b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonResponse.java new file mode 100644 index 0000000..b7a56eb --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonResponse.java @@ -0,0 +1,18 @@ +package com.codzilla.backend.controller.Sandbox.polygon; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +public class PolygonResponse { + private String status; + private String comment; + private ProblemResult result; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ProblemResult { + private Long id; + private String owner; + private String name; + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/problem/Problem.java b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/Problem.java new file mode 100644 index 0000000..aca79a0 --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/Problem.java @@ -0,0 +1,30 @@ +package com.codzilla.backend.controller.Sandbox.problem; + +import jakarta.persistence.*; +import lombok.Data; + +@Entity +@Table(name = "problems") +@Data +public class Problem { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String polygonToken; + + @Enumerated(EnumType.STRING) + private ProblemType type; + + @Enumerated(EnumType.STRING) + private ProblemLevel level; + + public enum ProblemType { + ALGORITHM, DATA_STRUCTURES, MATH + } + + public enum ProblemLevel { + EASY, MEDIUM, HARD + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemController.java b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemController.java new file mode 100644 index 0000000..aaecf63 --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemController.java @@ -0,0 +1,44 @@ +package com.codzilla.backend.controller.Sandbox.problem; + +import com.codzilla.backend.controller.Sandbox.polygon.CreateProblemRequest; +import com.codzilla.backend.kafka.SubmissionMessage; +import com.codzilla.backend.kafka.SubmissionProducer; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/problems") +@RequiredArgsConstructor +public class ProblemController { + + private final ProblemService problemService; + private final SubmissionProducer submissionProducer; + + @PostMapping("/create") + public ResponseEntity createProblem(@RequestBody CreateProblemRequest request) { + Problem saved = problemService.createProblem(request); + return ResponseEntity.ok(saved); + } + +// @PostMapping("/{id}/submit") +// public ResponseEntity submit( +// @PathVariable Long id, +// @RequestParam int languageId, +// @RequestBody String sourceCode) { +// String result = problemService.submitSolution(id, sourceCode, languageId); +// return ResponseEntity.ok(result); +// } + + @PostMapping("/{id}/submit") + public ResponseEntity submit( + @PathVariable Long id, + @RequestParam int languageId, + @RequestBody String sourceCode) { + String submissionId = UUID.randomUUID().toString(); + submissionProducer.send(new SubmissionMessage(submissionId, id, sourceCode, languageId)); + return ResponseEntity.ok(submissionId); + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemRepository.java b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemRepository.java new file mode 100644 index 0000000..9f4127d --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemRepository.java @@ -0,0 +1,18 @@ +package com.codzilla.backend.controller.Sandbox.problem; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProblemRepository extends JpaRepository { +} + +/* + JpaRepository уже даёт : + save(problem) — сохранить + findById(id) — найти по id + findAll() — все задачи + deleteById(id) — удалить +*/ + + diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemService.java b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemService.java new file mode 100644 index 0000000..2e75d2c --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemService.java @@ -0,0 +1,70 @@ +package com.codzilla.backend.controller.Sandbox.problem; + +import com.codzilla.backend.controller.Sandbox.judge0.Judge0Client; +import com.codzilla.backend.controller.Sandbox.polygon.CreateProblemRequest; +import com.codzilla.backend.controller.Sandbox.polygon.PolygonClient; +import com.codzilla.backend.controller.Sandbox.polygon.PolygonProblem; +import com.codzilla.backend.test.Test; +import com.codzilla.backend.test.TestRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +@Service +@RequiredArgsConstructor +@Slf4j +public class ProblemService { + + private final PolygonClient polygonClient; + private final ProblemRepository problemRepository; + private final Judge0Client judge0Client; + private final TestRepository testRepository; + + public Problem createProblem(CreateProblemRequest request) { + String polygonId = polygonClient.createProblem(request.getName()); + + Problem problem = new Problem(); + problem.setPolygonToken(polygonId); + problem.setType(request.getType()); + problem.setLevel(request.getLevel()); + Problem saved = problemRepository.save(problem); + + // сохраняем тесты в Polygon И в нашу БД + var tests = request.getTests(); + for (int i = 0; i < tests.size(); i++) { + var t = tests.get(i); + polygonClient.saveTest(polygonId, i + 1, t.getInput(), t.getOutput()); + + Test test = new Test(); + test.setProblemId(saved.getId()); + test.setTestIndex(i + 1); + test.setInput(t.getInput()); + test.setExpectedOutput(t.getOutput()); + testRepository.save(test); + } + + return saved; + } + + public String submitSolution(Long problemId, String sourceCode, int languageId) { + Problem problem = problemRepository.findById(problemId) + .orElseThrow(() -> new RuntimeException("Problem not found: " + problemId)); + + // берём тесты из НАШЕЙ БД + List tests = testRepository.findByProblemIdOrderByTestIndex(problemId); + if (tests.isEmpty()) { + throw new RuntimeException("No tests found for problem: " + problemId); + } + + List results = new ArrayList<>(); + for (Test test : tests) { + String verdict = judge0Client.submit(sourceCode, languageId, test.getInput()); + results.add("Test " + test.getTestIndex() + ": " + verdict); + if (!"Accepted".equals(verdict)) break; + } + + return String.join("\n", results); + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/kafka/SubmissionConsumer.java b/src/main/java/com/codzilla/backend/kafka/SubmissionConsumer.java new file mode 100644 index 0000000..eeb6d84 --- /dev/null +++ b/src/main/java/com/codzilla/backend/kafka/SubmissionConsumer.java @@ -0,0 +1,37 @@ +package com.codzilla.backend.kafka; + +import com.codzilla.backend.controller.Sandbox.problem.ProblemService; +import com.codzilla.backend.submission.SubmissionService; +import com.codzilla.backend.websocket.ResultWebSocketHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SubmissionConsumer { + + private final ProblemService problemService; + private final ResultWebSocketHandler webSocketHandler; + private final SubmissionService submissionService; + + @KafkaListener(topics = "submissions", groupId = "codzilla-group") + public void consume(SubmissionMessage message) { + log.info("Received from Kafka: {}", message.getSubmissionId()); + try { + String result = problemService.submitSolution( + message.getProblemId(), + message.getSourceCode(), + message.getLanguageId() + ); + submissionService.updateStatus(message.getSubmissionId(), result); + webSocketHandler.sendResult(message.getSubmissionId(), result); + } catch (Exception e) { + log.error("Error processing submission: {}", e.getMessage()); + submissionService.updateStatus(message.getSubmissionId(), "Error: " + e.getMessage()); + webSocketHandler.sendResult(message.getSubmissionId(), "Error: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/kafka/SubmissionMessage.java b/src/main/java/com/codzilla/backend/kafka/SubmissionMessage.java new file mode 100644 index 0000000..bf4425b --- /dev/null +++ b/src/main/java/com/codzilla/backend/kafka/SubmissionMessage.java @@ -0,0 +1,15 @@ +package com.codzilla.backend.kafka; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SubmissionMessage { + private String submissionId; + private Long problemId; + private String sourceCode; + private int languageId; +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/kafka/SubmissionProducer.java b/src/main/java/com/codzilla/backend/kafka/SubmissionProducer.java new file mode 100644 index 0000000..4e1bad9 --- /dev/null +++ b/src/main/java/com/codzilla/backend/kafka/SubmissionProducer.java @@ -0,0 +1,19 @@ +package com.codzilla.backend.kafka; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SubmissionProducer { + + private final KafkaTemplate kafkaTemplate; + + public void send(SubmissionMessage message) { + log.info("Sending to Kafka: {}", message.getSubmissionId()); + kafkaTemplate.send("submissions", message.getSubmissionId(), message); + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/repository/CoffeeRepository.java b/src/main/java/com/codzilla/backend/repository/CoffeeRepository.java deleted file mode 100644 index 5039e88..0000000 --- a/src/main/java/com/codzilla/backend/repository/CoffeeRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.codzilla.backend.repository; - -import com.codzilla.backend.controller.Coffee; -import org.springframework.data.repository.CrudRepository; - -public interface CoffeeRepository extends CrudRepository { -} diff --git a/src/main/java/com/codzilla/backend/submission/Submission.java b/src/main/java/com/codzilla/backend/submission/Submission.java new file mode 100644 index 0000000..36f6fef --- /dev/null +++ b/src/main/java/com/codzilla/backend/submission/Submission.java @@ -0,0 +1,22 @@ +package com.codzilla.backend.submission; + +import jakarta.persistence.*; +import lombok.Data; + +@Data +@Entity +@Table(name = "submissions") +public class Submission { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long problemId; + private int languageId; + + @Column(columnDefinition = "BYTEA") + private byte[] sourceCode; + + private String status; + private String submissionUuid; +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/submission/SubmissionController.java b/src/main/java/com/codzilla/backend/submission/SubmissionController.java new file mode 100644 index 0000000..ecca52d --- /dev/null +++ b/src/main/java/com/codzilla/backend/submission/SubmissionController.java @@ -0,0 +1,23 @@ +package com.codzilla.backend.submission; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/submissions") +@RequiredArgsConstructor +public class SubmissionController { + + private final SubmissionService submissionService; + + @PostMapping("/upload") + public ResponseEntity upload( + @RequestParam Long problemId, + @RequestParam int languageId, + @RequestParam MultipartFile file) throws Exception { + String submissionUuid = submissionService.upload(problemId, languageId, file); + return ResponseEntity.ok(submissionUuid); + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/submission/SubmissionRepository.java b/src/main/java/com/codzilla/backend/submission/SubmissionRepository.java new file mode 100644 index 0000000..4817a9f --- /dev/null +++ b/src/main/java/com/codzilla/backend/submission/SubmissionRepository.java @@ -0,0 +1,6 @@ +package com.codzilla.backend.submission; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SubmissionRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/submission/SubmissionService.java b/src/main/java/com/codzilla/backend/submission/SubmissionService.java new file mode 100644 index 0000000..881fb5c --- /dev/null +++ b/src/main/java/com/codzilla/backend/submission/SubmissionService.java @@ -0,0 +1,50 @@ +package com.codzilla.backend.submission; + +import com.codzilla.backend.kafka.SubmissionMessage; +import com.codzilla.backend.kafka.SubmissionProducer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SubmissionService { + + private final SubmissionRepository submissionRepository; + private final SubmissionProducer submissionProducer; + + public String upload(Long problemId, int languageId, MultipartFile file) throws Exception { + // 1. читаем байты файла + byte[] bytes = file.getBytes(); + + // 2. сохраняем в БД + String submissionUuid = UUID.randomUUID().toString(); + Submission submission = new Submission(); + submission.setProblemId(problemId); + submission.setLanguageId(languageId); + submission.setSourceCode(bytes); + submission.setStatus("PENDING"); + submission.setSubmissionUuid(submissionUuid); + submissionRepository.save(submission); + + // 3. отправляем в Kafka (байты → String) + String sourceCode = new String(bytes); + submissionProducer.send(new SubmissionMessage(submissionUuid, problemId, sourceCode, languageId)); + + return submissionUuid; + } + + public void updateStatus(String submissionUuid, String status) { + submissionRepository.findAll().stream() + .filter(s -> submissionUuid.equals(s.getSubmissionUuid())) + .findFirst() + .ifPresent(s -> { + s.setStatus(status); + submissionRepository.save(s); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/test/Test.java b/src/main/java/com/codzilla/backend/test/Test.java new file mode 100644 index 0000000..0049da1 --- /dev/null +++ b/src/main/java/com/codzilla/backend/test/Test.java @@ -0,0 +1,23 @@ +package com.codzilla.backend.test; + +import jakarta.persistence.*; +import lombok.Data; + +@Data +@Entity +@Table(name = "tests") +public class Test { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long problemId; + private int testIndex; + + @Column(columnDefinition = "TEXT") + private String input; + + @Column(columnDefinition = "TEXT") + private String expectedOutput; +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/test/TestRepository.java b/src/main/java/com/codzilla/backend/test/TestRepository.java new file mode 100644 index 0000000..2c38167 --- /dev/null +++ b/src/main/java/com/codzilla/backend/test/TestRepository.java @@ -0,0 +1,8 @@ +package com.codzilla.backend.test; + +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface TestRepository extends JpaRepository { + List findByProblemIdOrderByTestIndex(Long problemId); +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/websocket/ResultWebSocketHandler.java b/src/main/java/com/codzilla/backend/websocket/ResultWebSocketHandler.java new file mode 100644 index 0000000..517907f --- /dev/null +++ b/src/main/java/com/codzilla/backend/websocket/ResultWebSocketHandler.java @@ -0,0 +1,68 @@ +package com.codzilla.backend.websocket; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +public class ResultWebSocketHandler extends TextWebSocketHandler { + + private final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap pendingResults = new ConcurrentHashMap<>(); + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + String submissionId = getSubmissionId(session); + if (submissionId != null) { + sessions.put(submissionId, session); + log.info("WebSocket connected: {}", submissionId); + + + String pending = pendingResults.remove(submissionId); + if (pending != null) { + session.sendMessage(new TextMessage(pending)); + sessions.remove(submissionId); + } + } + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + String submissionId = getSubmissionId(session); + if (submissionId != null) { + sessions.remove(submissionId); + log.info("WebSocket disconnected: {}", submissionId); + } + } + + public void sendResult(String submissionId, String result) { + WebSocketSession session = sessions.get(submissionId); + if (session != null && session.isOpen()) { + try { + session.sendMessage(new TextMessage(result)); + sessions.remove(submissionId); + } catch (Exception e) { + log.error("Failed to send WebSocket message: {}", e.getMessage()); + } + } else { + + log.info("Session not ready, caching result for: {}", submissionId); + pendingResults.put(submissionId, result); + } + } + + private String getSubmissionId(WebSocketSession session) { + String query = session.getUri() != null ? session.getUri().getQuery() : null; + if (query != null && query.startsWith("submissionId=")) { + return query.substring("submissionId=".length()); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/websocket/WebSocketConfig.java b/src/main/java/com/codzilla/backend/websocket/WebSocketConfig.java new file mode 100644 index 0000000..9f4fdcb --- /dev/null +++ b/src/main/java/com/codzilla/backend/websocket/WebSocketConfig.java @@ -0,0 +1,21 @@ +package com.codzilla.backend.websocket; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +@Configuration +@EnableWebSocket +@RequiredArgsConstructor +public class WebSocketConfig implements WebSocketConfigurer { + + private final ResultWebSocketHandler resultWebSocketHandler; + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(resultWebSocketHandler, "/ws/results") + .setAllowedOrigins("*"); + } +} \ No newline at end of file diff --git a/src/main/resources/application-dev.properties.example b/src/main/resources/application-dev.properties.example deleted file mode 100644 index 959e765..0000000 --- a/src/main/resources/application-dev.properties.example +++ /dev/null @@ -1,4 +0,0 @@ -spring.datasource.url=jdbc:postgresql://localhost:5433/testingdb -spring.datasource.username=myuser -spring.datasource.password=secret -spring.jpa.hibernate.ddl-auto=update \ No newline at end of file diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 9ec9103..e027dc4 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -1,4 +1,31 @@ spring.datasource.url=${DB_URL} spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD} -spring.jpa.hibernate.ddl-auto=update \ No newline at end of file + +polygon.api.key=${POLYGON_KEY} +polygon.api.secret=${POLYGON_SECRET} + + +spring.kafka.bootstrap-servers=${KAFKA_BOOTSTRAP_SERVERS} +spring.kafka.consumer.group-id=codzilla-group +spring.kafka.consumer.auto-offset-reset=earliest +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer +spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer +spring.kafka.consumer.properties.spring.json.trusted.packages=* + + +# ????? ? docker-compose.yml ????? ????? ?? ?????????? ??? ??? +#backend: +#build: ./backend +#ports: +#- "8080:8080" +#environment: +#SPRING_PROFILES_ACTIVE: prod +#DB_URL: jdbc:postgresql://postgres:5432/codzilla +#DB_USERNAME: user +#DB_PASSWORD: password +#depends_on: +#- postgres +#- judge0-server \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7060d81..4347897 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,6 @@ spring.application.name=Backend -spring.jpa.show-sql=true \ No newline at end of file +spring.jpa.show-sql=true + +# ??????? +spring.jpa.hibernate.ddl-auto=update + diff --git a/src/test/java/com/codzilla/backend/controller/CoffeeControllerTest.java b/src/test/java/com/codzilla/backend/controller/CoffeeControllerTest.java deleted file mode 100644 index 7be3348..0000000 --- a/src/test/java/com/codzilla/backend/controller/CoffeeControllerTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.codzilla.backend.controller; - -import com.codzilla.backend.repository.CoffeeRepository; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class CoffeeControllerTest { - - @Autowired - private TestRestTemplate restTemplate; - - @Autowired - private CoffeeRepository coffeeRepository; - - @BeforeEach - void setUp() { - coffeeRepository.deleteAll(); - } - - @Test - void getCoffees_shouldReturnList() { - coffeeRepository.save(new Coffee("Espresso")); - coffeeRepository.save(new Coffee("Latte")); - - ResponseEntity response = restTemplate.getForEntity("/coffee", Coffee[].class); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).hasSize(2); - } - - @Test - void getCoffee_shouldReturnCoffee_whenExists() { - Coffee saved = coffeeRepository.save(new Coffee("Cappuccino")); - - ResponseEntity response = restTemplate.getForEntity("/coffee/" + saved.getId(), Coffee.class); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - Assertions.assertNotNull(response.getBody()); - assertThat(response.getBody().getName()).isEqualTo("Cappuccino"); - } - - @Test - void postCoffee_shouldCreateAndReturnCoffee() { - Coffee coffee = new Coffee("Mocha"); - - ResponseEntity response = restTemplate.postForEntity("/coffee", coffee, Coffee.class); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - Assertions.assertNotNull(response.getBody()); - assertThat(response.getBody().getName()).isEqualTo("Mocha"); - } - - @Test - void deleteCoffee_shouldRemoveCoffee() { - Coffee saved = coffeeRepository.save(new Coffee("ToDelete")); - - restTemplate.delete("/coffee/" + saved.getId()); - - assertThat(coffeeRepository.findById(saved.getId())).isEmpty(); - } -} \ No newline at end of file