Skip to content

Commit

Permalink
Add mjr endpoint (#70)
Browse files Browse the repository at this point in the history
* Add mjr endpoint

* serialize json

* code review

* Trivy

* Sonar coverage
  • Loading branch information
southeo authored Nov 14, 2023
1 parent 74acdf8 commit e9ac31f
Show file tree
Hide file tree
Showing 14 changed files with 554 additions and 37 deletions.
19 changes: 4 additions & 15 deletions .github/workflows/.trivyignore
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
# Date: June 16, 2023
# Issue: snappy-java high issue, https://www.cvedetails.com/cve/CVE-2023-34453/
# Solution: Spring needs to update its version of kafka
# CVE-2023-34455

# Date: August 24, 2023
# Issue: snappy-java used by kafka, which is used by spring-kafka
# Solution: Spring needs to update its version of kafka
# CVE-2023-34453
# CVE-2023-34454

# Date: August 25
# Manually set kafka version to 3.5.1. This version addresses the above CVEs
# Should remove this property once spring-kafka updates

# Date: November 14, 2023
# Issue: openssl: Incorrect cipher key and IV length processing https://avd.aquasec.com/nvd/cve-2023-5363
# Solution: Docker image needs an update
CVE-2023-5363
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.4</version>
<version>3.1.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>eu.dissco</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package eu.dissco.backend.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import eu.dissco.backend.domain.AnnotationState;
import eu.dissco.backend.domain.jsonapi.JsonApiListResponseWrapper;
import eu.dissco.backend.domain.jsonapi.JsonApiWrapper;
import eu.dissco.backend.exceptions.NotFoundException;
import eu.dissco.backend.properties.ApplicationProperties;
import eu.dissco.backend.service.MasJobRecordService;
import jakarta.servlet.http.HttpServletRequest;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
@RestController
@RequestMapping("api/v1/mjr")
public class MasJobRecordController extends BaseController {

private final MasJobRecordService service;

public MasJobRecordController(ObjectMapper mapper,
ApplicationProperties applicationProperties, MasJobRecordService service) {
super(mapper, applicationProperties);
this.service = service;
}

@GetMapping(value = "/{jobId}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<JsonApiWrapper> getMasJobRecord(
@PathVariable("jobId") UUID jobId, HttpServletRequest request) throws NotFoundException {
return ResponseEntity.ok().body(service.getMasJobRecordById(jobId, getPath(request)));
}

@GetMapping(value = "/creator/"
+ "{creatorId}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<JsonApiListResponseWrapper> getMasJobRecordsForCreator(
@PathVariable("creatorId") String creatorId,
@RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber,
@RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize,
@RequestParam(required = false) AnnotationState state,
HttpServletRequest request) {
return ResponseEntity.ok().body(
service.getMasJobRecordsByCreator(creatorId, getPath(request), pageNumber, pageSize,
state));

}

}
22 changes: 20 additions & 2 deletions src/main/java/eu/dissco/backend/domain/AnnotationState.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,30 @@
@Getter
public enum AnnotationState {
SCHEDULED("scheduled"),
FAILED("failed");
FAILED("failed"),
COMPLETED("completed");

private final String state;

AnnotationState(String s){
AnnotationState(String s) {
this.state = s;
}

public static AnnotationState fromString(String s) {
switch (s.toLowerCase()) {
case "scheduled" -> {
return SCHEDULED;
}
case "failed" -> {
return FAILED;
}
case "completed" -> {
return COMPLETED;
}
default ->
throw new IllegalStateException("Unable to construct AnnotationState from state " + s);
}

}

}
17 changes: 17 additions & 0 deletions src/main/java/eu/dissco/backend/domain/MasJobRecordFull.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package eu.dissco.backend.domain;

import com.fasterxml.jackson.databind.JsonNode;
import java.time.Instant;
import java.util.UUID;

public record MasJobRecordFull(
AnnotationState state,
String creatorId,
String targetId,
UUID jobId,
Instant timeStarted,
Instant timeCompleted,
JsonNode annotations
) {

}
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
package eu.dissco.backend.repository;

import static eu.dissco.backend.database.jooq.Tables.MAS_JOB_RECORD;
import static eu.dissco.backend.repository.RepositoryUtils.getOffset;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import eu.dissco.backend.domain.AnnotationState;
import eu.dissco.backend.domain.MasJobRecord;
import eu.dissco.backend.domain.MasJobRecordFull;
import eu.dissco.backend.exceptions.DisscoJsonBMappingException;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.jooq.DSLContext;
import org.jooq.Record;
import org.jooq.Record4;
import org.springframework.stereotype.Repository;

Expand All @@ -18,31 +26,80 @@
public class MasJobRecordRepository {

private final DSLContext context;
private final ObjectMapper mapper;

public Map<String, UUID> createNewMasJobRecord(List<MasJobRecord> masJobRecord){
public Optional<MasJobRecordFull> getMasJobRecordById(UUID masJobRecordId) {
return context.select(MAS_JOB_RECORD.asterisk())
.from(MAS_JOB_RECORD)
.where(MAS_JOB_RECORD.JOB_ID.eq(masJobRecordId))
.fetchOptional(this::recordToMasJobRecord);
}

public List<MasJobRecordFull> getMasJobRecordsByCreator(String creatorId, int pageNum, int pageSize){
var offset = getOffset(pageNum, pageSize);
return context.select(MAS_JOB_RECORD.asterisk())
.from(MAS_JOB_RECORD)
.where(MAS_JOB_RECORD.CREATOR_ID.eq(creatorId))
.limit(pageSize)
.offset(offset)
.fetch(this::recordToMasJobRecord);
}

public List<MasJobRecordFull> getMasJobRecordsByCreatorAndStatus(String creatorId, String state, int pageNum, int pageSize){
var offset = getOffset(pageNum, pageSize);
return context.select(MAS_JOB_RECORD.asterisk())
.from(MAS_JOB_RECORD)
.where(MAS_JOB_RECORD.CREATOR_ID.eq(creatorId))
.and(MAS_JOB_RECORD.STATE.eq(state))
.limit(pageSize)
.offset(offset)
.fetch(this::recordToMasJobRecord);
}


public Map<String, UUID> createNewMasJobRecord(List<MasJobRecord> masJobRecord) {
var records = masJobRecord.stream().map(this::mjrToRecord).toList();

return context.insertInto(MAS_JOB_RECORD, MAS_JOB_RECORD.STATE, MAS_JOB_RECORD.CREATOR_ID, MAS_JOB_RECORD.TARGET_ID, MAS_JOB_RECORD.TIME_STARTED)
return context.insertInto(MAS_JOB_RECORD, MAS_JOB_RECORD.STATE, MAS_JOB_RECORD.CREATOR_ID,
MAS_JOB_RECORD.TARGET_ID, MAS_JOB_RECORD.TIME_STARTED)
.valuesOfRecords(records)
.returning(MAS_JOB_RECORD.CREATOR_ID, MAS_JOB_RECORD.JOB_ID)
.fetchMap(MAS_JOB_RECORD.CREATOR_ID, MAS_JOB_RECORD.JOB_ID);
}

public void markMasJobRecordsAsFailed(List<UUID> ids){
public void markMasJobRecordsAsFailed(List<UUID> ids) {
context.update(MAS_JOB_RECORD)
.set(MAS_JOB_RECORD.STATE, AnnotationState.FAILED.getState())
.set(MAS_JOB_RECORD.TIME_COMPLETED, Instant.now())
.where(MAS_JOB_RECORD.JOB_ID.in(ids))
.execute();
}

private Record4<String, String, String, Instant> mjrToRecord(MasJobRecord masJobRecord){
var dbRecord = context.newRecord(MAS_JOB_RECORD.STATE, MAS_JOB_RECORD.CREATOR_ID, MAS_JOB_RECORD.TARGET_ID, MAS_JOB_RECORD.TIME_STARTED);
private Record4<String, String, String, Instant> mjrToRecord(MasJobRecord masJobRecord) {
var dbRecord = context.newRecord(MAS_JOB_RECORD.STATE, MAS_JOB_RECORD.CREATOR_ID,
MAS_JOB_RECORD.TARGET_ID, MAS_JOB_RECORD.TIME_STARTED);
dbRecord.set(MAS_JOB_RECORD.STATE, masJobRecord.state().getState());
dbRecord.set(MAS_JOB_RECORD.CREATOR_ID, masJobRecord.creatorId());
dbRecord.set(MAS_JOB_RECORD.TARGET_ID, masJobRecord.targetId());
dbRecord.set(MAS_JOB_RECORD.TIME_STARTED, Instant.now());
return dbRecord;
}

private MasJobRecordFull recordToMasJobRecord(Record dbRecord) {
try {
return new MasJobRecordFull(
AnnotationState.fromString(dbRecord.get(MAS_JOB_RECORD.STATE)),
dbRecord.get(MAS_JOB_RECORD.CREATOR_ID),
dbRecord.get(MAS_JOB_RECORD.TARGET_ID),
dbRecord.get(MAS_JOB_RECORD.JOB_ID),
dbRecord.get(MAS_JOB_RECORD.TIME_STARTED),
dbRecord.get(MAS_JOB_RECORD.TIME_COMPLETED),
mapper.readValue(dbRecord.get(MAS_JOB_RECORD.ANNOTATIONS).data(), JsonNode.class)
);
} catch (JsonProcessingException e) {
throw new DisscoJsonBMappingException("Unable to parse annotations from MAS job record", e);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,11 @@
import eu.dissco.backend.repository.MongoRepository;
import java.io.IOException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.kafka.common.protocol.types.Field.Str;
import org.springframework.stereotype.Service;

@Slf4j
Expand All @@ -47,7 +45,6 @@ public class AnnotationService {
private final MongoRepository mongoRepository;
private final UserService userService;
private final ObjectMapper mapper;
private final DateTimeFormatter formatter;

public JsonApiWrapper getAnnotation(String id, String path) {
var annotation = repository.getAnnotation(id);
Expand Down
54 changes: 52 additions & 2 deletions src/main/java/eu/dissco/backend/service/MasJobRecordService.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
package eu.dissco.backend.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import eu.dissco.backend.domain.AnnotationState;
import eu.dissco.backend.domain.MachineAnnotationServiceRecord;
import eu.dissco.backend.domain.MasJobRecord;
import eu.dissco.backend.domain.MasJobRecordFull;
import eu.dissco.backend.domain.jsonapi.JsonApiData;
import eu.dissco.backend.domain.jsonapi.JsonApiLinks;
import eu.dissco.backend.domain.jsonapi.JsonApiLinksFull;
import eu.dissco.backend.domain.jsonapi.JsonApiListResponseWrapper;
import eu.dissco.backend.domain.jsonapi.JsonApiWrapper;
import eu.dissco.backend.exceptions.NotFoundException;
import eu.dissco.backend.repository.MasJobRecordRepository;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

Expand All @@ -16,15 +26,55 @@
public class MasJobRecordService {

private final MasJobRecordRepository masJobRecordRepository;
private final ObjectMapper mapper;

public JsonApiWrapper getMasJobRecordById(UUID masJobRecordId, String path)
throws NotFoundException {
var masJobRecordOptional = masJobRecordRepository.getMasJobRecordById(masJobRecordId);
if (masJobRecordOptional.isEmpty()) {
throw new NotFoundException(
"Unable to find MAS Job Record for job " + masJobRecordId.toString());
}
var masJobRecord = masJobRecordOptional.get();
var dataNode = new JsonApiData(masJobRecordId.toString(), "masJobRecord",
mapper.valueToTree(masJobRecord));
return new JsonApiWrapper(dataNode, new JsonApiLinks(path));
}

public JsonApiListResponseWrapper getMasJobRecordsByCreator(String creatorId, String path,
int pageNum, int pageSize, AnnotationState state) {
int pageSizeToCheckNext = pageSize + 1;
List<MasJobRecordFull> masJobRecordsPlusOne;
if (state == null) {
masJobRecordsPlusOne = masJobRecordRepository.getMasJobRecordsByCreator(creatorId, pageNum,
pageSizeToCheckNext);
} else {
masJobRecordsPlusOne = masJobRecordRepository.getMasJobRecordsByCreatorAndStatus(creatorId,
state.getState(), pageNum, pageSizeToCheckNext);
}
boolean hasNext = masJobRecordsPlusOne.size() > pageSize;
var masJobRecords = hasNext ? masJobRecordsPlusOne.subList(0, pageSize) : masJobRecordsPlusOne;
List<JsonApiData> dataList = masJobRecords.stream().map(
mjr -> new JsonApiData(mjr.jobId().toString(), "masJobRecord", mapper.valueToTree(mjr)))
.toList();
JsonApiLinksFull linksNode;
if (masJobRecords.isEmpty()) {
linksNode = new JsonApiLinksFull(path);
} else {
linksNode = new JsonApiLinksFull(pageSize, pageNum, hasNext, path);
}
return new JsonApiListResponseWrapper(dataList, linksNode);
}

public Map<String, UUID> createMasJobRecord(Set<MachineAnnotationServiceRecord> masRecords, String targetId) {
public Map<String, UUID> createMasJobRecord(Set<MachineAnnotationServiceRecord> masRecords,
String targetId) {
var masJobRecordList = masRecords.stream()
.map(masRecord -> new MasJobRecord(AnnotationState.SCHEDULED, masRecord.id(), targetId))
.toList();
return masJobRecordRepository.createNewMasJobRecord(masJobRecordList);
}

public void markMasJobRecordAsFailed(List<UUID> failedJobIds){
public void markMasJobRecordAsFailed(List<UUID> failedJobIds) {
masJobRecordRepository.markMasJobRecordsAsFailed(failedJobIds);
}

Expand Down
Loading

0 comments on commit e9ac31f

Please sign in to comment.