Skip to content
Merged
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
40 changes: 40 additions & 0 deletions src/main/java/com/kw/readwith/aws/s3/AmazonS3Manager.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.model.ListObjectsV2Request;
import com.amazonaws.services.s3.model.ListObjectsV2Result;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.S3Object;
import com.kw.readwith.config.AmazonConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -15,8 +18,10 @@
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import java.util.UUID;

@Slf4j
Expand Down Expand Up @@ -58,6 +63,36 @@ public byte[] downloadFile(String keyName) {
}
}

public <T> T readObject(String keyName, S3ObjectReader<T> reader) throws IOException {
try (S3Object object = amazonS3.getObject(amazonConfig.getBucket(), keyName);
InputStream inputStream = object.getObjectContent()) {
return reader.read(inputStream);
}
}

public List<String> listKeys(String prefix) {
List<String> keys = new ArrayList<>();
ListObjectsV2Request request = new ListObjectsV2Request()
.withBucketName(amazonConfig.getBucket())
.withPrefix(prefix);

ListObjectsV2Result result;
do {
result = amazonS3.listObjectsV2(request);
result.getObjectSummaries().forEach(summary -> keys.add(summary.getKey()));
request.setContinuationToken(result.getNextContinuationToken());
} while (result.isTruncated());

return keys;
}

public void deleteKeys(List<String> keyNames) {
if (keyNames == null || keyNames.isEmpty()) {
return;
}
keyNames.forEach(keyName -> amazonS3.deleteObject(amazonConfig.getBucket(), keyName));
}

public String getObjectUrl(String keyName) {
return amazonS3.getUrl(amazonConfig.getBucket(), keyName).toString();
}
Expand Down Expand Up @@ -118,4 +153,9 @@ private String generateUniqueFileName(String originalFilename) {
}
return UUID.randomUUID() + ext;
}

@FunctionalInterface
public interface S3ObjectReader<T> {
T read(InputStream inputStream) throws IOException;
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/kw/readwith/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class SwaggerConfig {

Expand All @@ -34,4 +37,21 @@ public OpenAPI melissaServerAPI() {
.addSecurityItem(securityRequirement)
.components(components);
}

@Bean
public OpenApiCustomizer hideLegacyApiAliases() {
return openApi -> {
if (openApi.getPaths() == null) {
return;
}

List<String> legacyAliases = openApi.getPaths().keySet().stream()
.filter(path -> path.startsWith("/api/"))
.filter(path -> !path.startsWith("/api/v2/"))
.filter(path -> openApi.getPaths().containsKey("/api/v2" + path.substring("/api".length())))
.toList();

legacyAliases.forEach(openApi.getPaths()::remove);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.kw.readwith.dto.admin;

import com.kw.readwith.domain.enums.ProcessingJobStatus;
import com.kw.readwith.domain.enums.ProcessingPipelineType;
import com.kw.readwith.domain.processing.ProcessingJob;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProcessingJobResponseDTO {
private Long id;
private Long bookId;
private String bookTitle;
private ProcessingPipelineType pipelineType;
private String runId;
private String sourceVersion;
private String artifactPath;
private ProcessingJobStatus status;
private String currentStep;
private String failureCode;
private String failureMessage;
private String triggeredBy;
private String ruleVersion;
private String locatorVersion;
private LocalDateTime createdAt;
private LocalDateTime startedAt;
private LocalDateTime finishedAt;

public static ProcessingJobResponseDTO from(ProcessingJob job) {
return ProcessingJobResponseDTO.builder()
.id(job.getId())
.bookId(job.getBook().getId())
.bookTitle(job.getBook().getTitle())
.pipelineType(job.getPipelineType())
.runId(job.getRunId())
.sourceVersion(job.getSourceVersion())
.artifactPath(job.getArtifactPath())
.status(job.getStatus())
.currentStep(job.getCurrentStep())
.failureCode(job.getFailureCode())
.failureMessage(job.getFailureMessage())
.triggeredBy(job.getTriggeredBy())
.ruleVersion(job.getRuleVersion())
.locatorVersion(job.getLocatorVersion())
.createdAt(job.getCreatedAt())
.startedAt(job.getStartedAt())
.finishedAt(job.getFinishedAt())
.build();
}
}
69 changes: 46 additions & 23 deletions src/main/java/com/kw/readwith/service/AdminService.java
Original file line number Diff line number Diff line change
Expand Up @@ -406,28 +406,11 @@ public int uploadRelationshipDeltas(Long bookId, List<MultipartFile> files) {

try {
RelationshipUploadDTO dto = objectMapper.readValue(file.getInputStream(), RelationshipUploadDTO.class);
if (dto == null || dto.getChapterIndex() == null) {
throw new GeneralException(ErrorStatus._BAD_REQUEST, "relationship delta payload chapterIndex is required.");
RelationshipDeltaCandidate candidate = resolveRelationshipDeltaCandidate(bookId, book, dto);
if (candidatesByEventId.containsKey(candidate.eventId())) {
throw new GeneralException(ErrorStatus._BAD_REQUEST, "Duplicate relationship delta eventId in request: " + candidate.eventId());
}
validateRelationshipDeltaContract(dto);

String eventId = requireText(dto.getEventId(), "relationship.eventId");
int chapterIdx = dto.getChapterIndex();
int eventIdx = parseEventIdx(eventId, chapterIdx);

Event event = eventRepository.findByBookIdAndChapterIdxAndEventIdx(bookId, chapterIdx, eventIdx)
.orElseThrow(() -> new GeneralException(
ErrorStatus.EVENT_NOT_FOUND,
String.format("Event not found for book=%d chapter=%d event=%d", bookId, chapterIdx, eventIdx)
));

String normalizedEventId = normalizeEventId(eventId, chapterIdx, eventIdx);
if (candidatesByEventId.containsKey(normalizedEventId)) {
throw new GeneralException(ErrorStatus._BAD_REQUEST, "Duplicate relationship delta eventId in request: " + normalizedEventId);
}

RelationshipDeltaCandidate candidate = buildRelationshipDeltaCandidate(event, book, dto);
candidatesByEventId.put(normalizedEventId, candidate);
candidatesByEventId.put(candidate.eventId(), candidate);
} catch (IOException e) {
throw new GeneralException(ErrorStatus.JSON_PARSING_ERROR, "Failed to parse relationship delta JSON: " + file.getOriginalFilename());
}
Expand All @@ -446,17 +429,56 @@ public int uploadRelationshipDeltas(Long bookId, List<MultipartFile> files) {
}
}

@Transactional
public int replaceRelationshipDeltaPayload(Long bookId, RelationshipUploadDTO dto, boolean refreshStatus) {
Book book = bookRepository.findById(bookId)
.orElseThrow(() -> new GeneralException(ErrorStatus.BOOK_NOT_FOUND));

try {
RelationshipDeltaCandidate candidate = resolveRelationshipDeltaCandidate(bookId, book, dto);
replaceRelationshipDelta(candidate.event());
int savedCount = saveRelationshipDelta(candidate);
if (refreshStatus) {
bookAnalysisStatusService.refreshStatus(bookId);
}
return savedCount;
} catch (GeneralException e) {
markAnalysisRejectedIfNeeded(book, e);
throw e;
}
}

private RelationshipDeltaCandidate resolveRelationshipDeltaCandidate(Long bookId, Book book, RelationshipUploadDTO dto) {
if (dto == null || dto.getChapterIndex() == null) {
throw new GeneralException(ErrorStatus._BAD_REQUEST, "relationship delta payload chapterIndex is required.");
}
validateRelationshipDeltaContract(dto);

String eventId = requireText(dto.getEventId(), "relationship.eventId");
int chapterIdx = dto.getChapterIndex();
int eventIdx = parseEventIdx(eventId, chapterIdx);

Event event = eventRepository.findByBookIdAndChapterIdxAndEventIdx(bookId, chapterIdx, eventIdx)
.orElseThrow(() -> new GeneralException(
ErrorStatus.EVENT_NOT_FOUND,
String.format("Event not found for book=%d chapter=%d event=%d", bookId, chapterIdx, eventIdx)
));

String normalizedEventId = normalizeEventId(eventId, chapterIdx, eventIdx);
return buildRelationshipDeltaCandidate(normalizedEventId, event, book, dto);
}

private void validateRelationshipDeltaContract(RelationshipUploadDTO dto) {
String contractVersion = requireText(dto.getContractVersion(), "relationship.contractVersion");
if (!RELATIONSHIP_DELTA_CONTRACT_VERSION.equals(contractVersion)) {
throw new GeneralException(ErrorStatus._BAD_REQUEST, "Unsupported relationship contractVersion: " + contractVersion);
}
}

private RelationshipDeltaCandidate buildRelationshipDeltaCandidate(Event event, Book book, RelationshipUploadDTO dto) {
private RelationshipDeltaCandidate buildRelationshipDeltaCandidate(String eventId, Event event, Book book, RelationshipUploadDTO dto) {
List<EventCharacterStat> stats = buildRelationshipNodeWeights(event, book, dto.getNodeWeights());
List<EventRelationshipEdge> edges = buildRelationshipDeltaEdges(event, book, dto.getItems());
return new RelationshipDeltaCandidate(event, stats, edges);
return new RelationshipDeltaCandidate(eventId, event, stats, edges);
}

private List<EventCharacterStat> buildRelationshipNodeWeights(Event event, Book book, Map<String, NodeWeightDTO> nodeWeightsMap) {
Expand Down Expand Up @@ -572,6 +594,7 @@ private int saveRelationshipDelta(RelationshipDeltaCandidate candidate) {
}

private record RelationshipDeltaCandidate(
String eventId,
Event event,
List<EventCharacterStat> stats,
List<EventRelationshipEdge> edges
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.kw.readwith.service;

import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class RelationshipDeltaImportJobDispatcher {

private final RelationshipDeltaImportJobService relationshipDeltaImportJobService;

@Async("normalizationJobExecutor")
public void dispatch(Long jobId) {
relationshipDeltaImportJobService.execute(jobId);
}
}
Loading
Loading