Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upsert specimens #73

Merged
merged 3 commits into from
Dec 15, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public enum FdoProfile {
private final int index;


private FdoProfile(String attribute, int index) {
FdoProfile(String attribute, int index) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Way are these no longer private?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"If you do not specify an access modifier the enum constructor it will be implicitly private."

Source

Slowly going to remove the private constructors when I touch enum files

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find 💯

this.attribute = attribute;
this.index = index;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package eu.dissco.core.handlemanager.domain.requests.objects;

public record DigitalSpecimenUpdateWrapper(
String handle,
DigitalSpecimenRequest digitalSpecimenRequest
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package eu.dissco.core.handlemanager.domain.requests.objects;

import java.util.List;

public record ProcessedDigitalSpecimenRequest(
List<DigitalSpecimenRequest> newRequests,
List<DigitalSpecimenUpdateWrapper> updateRequests
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

public enum BaseTypeOfSpecimen {
@JsonProperty("Material entity") MATERIAL("Material entity"),
@JsonProperty("Information artefact") INFO("informationArtefact");
@JsonProperty("Information artefact") INFO("Information Artefact");

private final String state;


private BaseTypeOfSpecimen(String state) {
BaseTypeOfSpecimen(String state) {
this.state = state;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ public enum MaterialSampleType {
@JsonProperty("Whole organism specimen") WHOLE_ORG("Whole organism specimen"),
@JsonProperty("Organism part") ORG_PART("Organism part"),
@JsonProperty("Organism product") ORG_PRODUCT("Organism product"),
@JsonProperty("Biome aggegation") AGGR_BIOME("Biome aggegation"),
@JsonProperty("Biome aggregation") AGGR_BIOME("Biome aggregation"),
@JsonProperty("Bundle biome aggregation") BUNDLE_BIOME("Bundle biome aggregation"),
@JsonProperty("Fossil") FOSSIL("Fossil"),
@JsonProperty("Any biological specimen") ANY_BIO("Any biological specimen"),
@JsonProperty("Aggregation") AGGR("Aggregation"),
@JsonProperty("Slurry biome aggegation") SLURRY_BIOME("Slurry biome aggegation"),
@JsonProperty("Slurry biome aggregation") SLURRY_BIOME("Slurry biome aggregation"),
@JsonProperty("Other solid object") OTHER_SOLID("Other solid object"),
@JsonProperty("Fluid in container") FLUID("Fluid in container"),
@JsonProperty("Anthropogenic aggregation") ANTHRO_AGGR("Anthropogenic aggregation"),
Expand All @@ -22,7 +22,7 @@ public enum MaterialSampleType {

private final String state;

private MaterialSampleType(String state) {
MaterialSampleType(String state) {
this.state = state;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static eu.dissco.core.handlemanager.domain.FdoProfile.PID_RECORD_ISSUE_NUMBER;
import static eu.dissco.core.handlemanager.domain.FdoProfile.PID_STATUS;
import static eu.dissco.core.handlemanager.domain.FdoProfile.PRIMARY_SPECIMEN_OBJECT_ID;
import static org.jooq.impl.DSL.select;

import eu.dissco.core.handlemanager.domain.repsitoryobjects.HandleAttribute;
import eu.dissco.core.handlemanager.exceptions.PidCreationException;
Expand Down Expand Up @@ -73,8 +74,18 @@ public List<HandleAttribute> resolveHandleAttributes(List<byte[]> handles) {

public List<HandleAttribute> searchByNormalisedPhysicalIdentifier(
List<byte[]> normalisedPhysicalIdentifiers) {

return searchByNormalisedPhysicalIdentifierQuery(normalisedPhysicalIdentifiers)
return context.select(HANDLES.IDX, HANDLES.HANDLE, HANDLES.TYPE, HANDLES.DATA)
.from(HANDLES)
.where(HANDLES.HANDLE.in(select(HANDLES.HANDLE).from(HANDLES)
.where(HANDLES.TYPE.eq(PID_STATUS.get().getBytes(StandardCharsets.UTF_8)))
.and(HANDLES.DATA.notEqual("ARCHIVED".getBytes(StandardCharsets.UTF_8)))
.and(HANDLES.HANDLE.in(
select(HANDLES.HANDLE).from(HANDLES)
.where(HANDLES.TYPE.eq(NORMALISED_SPECIMEN_OBJECT_ID.get().getBytes(
StandardCharsets.UTF_8)))
.and(HANDLES.DATA.in(normalisedPhysicalIdentifiers))
))))
.and(HANDLES.TYPE.eq(NORMALISED_SPECIMEN_OBJECT_ID.get().getBytes(StandardCharsets.UTF_8)))
.fetch(this::mapToAttribute);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ public JsonApiWrapperWrite createRecords(List<JsonNode> requests)
List<HandleAttribute> handleAttributes;
try {
switch (type) {
case DIGITAL_SPECIMEN ->
handleAttributes = createDigitalSpecimen(requestAttributes, handles);
case DIGITAL_SPECIMEN -> {
return upsertDigitalSpecimen(requestAttributes, handles);
}
case MEDIA_OBJECT -> handleAttributes = createMediaObject(requestAttributes, handles);
default -> throw new UnsupportedOperationException(
type + " is not an appropriate Type for DOI endpoint.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ public JsonApiWrapperWrite createRecords(List<JsonNode> requests)
try {
switch (type) {
case ANNOTATION -> handleAttributes = createAnnotation(requestAttributes, handles);
case DIGITAL_SPECIMEN ->
handleAttributes = createDigitalSpecimen(requestAttributes, handles);
case DIGITAL_SPECIMEN -> {
return upsertDigitalSpecimen(requestAttributes, handles);
}
case DOI -> handleAttributes = createDoi(requestAttributes, handles);
case HANDLE -> handleAttributes = createHandle(requestAttributes, handles);
case MAPPING -> handleAttributes = createMapping(requestAttributes, handles);
Expand Down
102 changes: 75 additions & 27 deletions src/main/java/eu/dissco/core/handlemanager/service/PidService.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
import eu.dissco.core.handlemanager.domain.jsonapi.JsonApiWrapperWrite;
import eu.dissco.core.handlemanager.domain.repsitoryobjects.HandleAttribute;
import eu.dissco.core.handlemanager.domain.requests.objects.DigitalSpecimenRequest;
import eu.dissco.core.handlemanager.domain.requests.objects.DigitalSpecimenUpdateWrapper;
import eu.dissco.core.handlemanager.domain.requests.objects.MediaObjectRequest;
import eu.dissco.core.handlemanager.domain.requests.objects.ProcessedDigitalSpecimenRequest;
import eu.dissco.core.handlemanager.domain.requests.vocabulary.specimen.ObjectType;
import eu.dissco.core.handlemanager.exceptions.InvalidRequestException;
import eu.dissco.core.handlemanager.exceptions.PidCreationException;
Expand All @@ -38,7 +40,7 @@
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
Expand Down Expand Up @@ -280,36 +282,84 @@ protected ObjectType getObjectType(List<JsonNode> requests) {
return ObjectType.fromString(type.get());
}

protected ArrayList<HandleAttribute> createDigitalSpecimen(List<JsonNode> requestAttributes,
protected JsonApiWrapperWrite upsertDigitalSpecimen(List<JsonNode> requestAttributes,
Iterator<byte[]> handleIterator)
throws InvalidRequestException, JsonProcessingException, PidResolutionException {
var handleAttributes = new ArrayList<HandleAttribute>();
var physicalIds = new ArrayList<byte[]>();
throws InvalidRequestException, JsonProcessingException, PidResolutionException, PidCreationException {
var specimenRequests = new ArrayList<DigitalSpecimenRequest>();
for (var request : requestAttributes) {
specimenRequests.add(mapper.treeToValue(request, DigitalSpecimenRequest.class));
}
var processResult = processSpecimenRequests(specimenRequests);
var recordTimeStamp = Instant.now().getEpochSecond();
var pidAttributes = createNewDigitalSpecimenRecords(processResult.newRequests(), handleIterator,
recordTimeStamp);
pidAttributes.addAll(updateDigitalSpecimen(processResult.updateRequests(), recordTimeStamp));
return new JsonApiWrapperWrite(formatCreateRecordsSpecimen(mapRecords(pidAttributes)));
}

private ArrayList<HandleAttribute> createNewDigitalSpecimenRecords(
List<DigitalSpecimenRequest> specimenRequests, Iterator<byte[]> handleIterator,
long recordTimestamp)
throws PidResolutionException, InvalidRequestException, PidCreationException {
if (specimenRequests.isEmpty()) {
return new ArrayList<>();
}
var handleAttributes = new ArrayList<HandleAttribute>();
for (var request : specimenRequests) {
var thisHandle = handleIterator.next();
var requestObject = mapper.treeToValue(request, DigitalSpecimenRequest.class);
physicalIds.add(
requestObject.getNormalisedPrimarySpecimenObjectId().getBytes(StandardCharsets.UTF_8));
handleAttributes.addAll(
fdoRecordService.prepareDigitalSpecimenRecordAttributes(requestObject, thisHandle));
fdoRecordService.prepareDigitalSpecimenRecordAttributes(request, thisHandle));
}
verifyNoRegisteredSpecimens(physicalIds);
log.info("Posting {} new digital specimen fdo records to db", specimenRequests.size());
pidRepository.postAttributesToDb(recordTimestamp, handleAttributes);
return handleAttributes;
}

protected void verifyNoRegisteredSpecimens(List<byte[]> physicalIds)
throws PidResolutionException {
var registeredRows = pidRepository.searchByNormalisedPhysicalIdentifier(
protected List<HandleAttribute> updateDigitalSpecimen(
List<DigitalSpecimenUpdateWrapper> updateRequests, long recordTimestamp)
throws InvalidRequestException, PidResolutionException {
if (updateRequests.isEmpty()) {
return Collections.emptyList();
}
List<List<HandleAttribute>> attributesToUpdate = new ArrayList<>();
List<HandleAttribute> flatList = new ArrayList<>();
for (var request : updateRequests) {
var requestAttributes = mapper.valueToTree(request.digitalSpecimenRequest());
flatList.addAll(fdoRecordService.prepareUpdateAttributes(
request.handle().getBytes(StandardCharsets.UTF_8), requestAttributes, DIGITAL_SPECIMEN));
attributesToUpdate.add(flatList);
}
log.info("Updating {} digital specimen fdo records to db", updateRequests.size());
pidRepository.updateRecordBatch(recordTimestamp, attributesToUpdate, true);
return flatList;
}

private ProcessedDigitalSpecimenRequest processSpecimenRequests(
ArrayList<DigitalSpecimenRequest> specimenRequests) {
var physicalIds = specimenRequests.stream()
.map(request -> request.getNormalisedPrimarySpecimenObjectId().getBytes(
StandardCharsets.UTF_8)).toList();
var registeredPhysicalIdentifiers = pidRepository.searchByNormalisedPhysicalIdentifier(
physicalIds);
if (!registeredRows.isEmpty()) {
var registeredHandles = registeredRows.stream()
.map(row -> new String(row.getHandle(), StandardCharsets.UTF_8)).toList();
log.error("Attempting to register identifiers for existing records");
log.debug("Handles already registered: {}", registeredHandles);
throw new PidResolutionException(
"Unable to create PID records. Some requested records are already registered. Verify the following digital specimens:"
+ registeredHandles);
var registeredPhysicalIdentiferMap = registeredPhysicalIdentifiers.stream()
.collect(Collectors.toMap(row -> new String(row.getData(), StandardCharsets.UTF_8),
row -> new String(row.getHandle(), StandardCharsets.UTF_8)));
var updates = specimenRequests.stream()
.filter(request -> registeredPhysicalIdentiferMap.containsKey(
request.getNormalisedPrimarySpecimenObjectId()))
.map(request -> new DigitalSpecimenUpdateWrapper(
registeredPhysicalIdentiferMap.get(request.getNormalisedPrimarySpecimenObjectId()),
request
))
.toList();
if (!updates.isEmpty()) {
log.debug("Existing records: {}",
updates.stream().map(DigitalSpecimenUpdateWrapper::handle).toList());
}
specimenRequests.removeAll(
updates.stream().map(DigitalSpecimenUpdateWrapper::digitalSpecimenRequest).toList());

return new ProcessedDigitalSpecimenRequest(specimenRequests, updates);
}

protected List<HandleAttribute> createMediaObject(List<JsonNode> requestAttributes,
Expand All @@ -332,22 +382,21 @@ public JsonApiWrapperWrite updateRecords(List<JsonNode> requests,
var recordTimestamp = Instant.now().getEpochSecond();
List<byte[]> handles = new ArrayList<>();
List<List<HandleAttribute>> attributesToUpdate = new ArrayList<>();
Map<String, ObjectType> recordTypes = new HashMap<>();
var recordType = getObjectType(requests);
for (JsonNode root : requests) {
JsonNode data = root.get(NODE_DATA);
byte[] handle = data.get(NODE_ID).asText().getBytes(StandardCharsets.UTF_8);
handles.add(handle);
JsonNode requestAttributes = data.get(NODE_ATTRIBUTES);
ObjectType type = ObjectType.fromString(data.get(NODE_TYPE).asText());
recordTypes.put(new String(handle, StandardCharsets.UTF_8), type);
var attributes = fdoRecordService.prepareUpdateAttributes(handle, requestAttributes, type);
attributesToUpdate.add(attributes);
}
checkInternalDuplicates(handles);
checkHandlesWritable(handles);

pidRepository.updateRecordBatch(recordTimestamp, attributesToUpdate, incrementVersion);
return formatUpdates(attributesToUpdate, recordTypes);
return formatUpdates(attributesToUpdate, recordType);
}

protected void checkInternalDuplicates(List<byte[]> handles) throws InvalidRequestException {
Expand Down Expand Up @@ -375,13 +424,12 @@ private Set<String> findDuplicates(List<byte[]> handles, Set<String> handlesToUp
}

protected JsonApiWrapperWrite formatUpdates(List<List<HandleAttribute>> updatedRecords,
Map<String, ObjectType> recordTypes) {
ObjectType type) {
List<JsonApiDataLinks> dataList = new ArrayList<>();
for (var updatedRecord : updatedRecords) {
String handle = new String(updatedRecord.get(0).getHandle(), StandardCharsets.UTF_8);
var attributeNode = jsonFormatSingleRecord(updatedRecord);
var type = recordTypes.get(handle).toString();
dataList.add(new JsonApiDataLinks(handle, type, attributeNode,
dataList.add(new JsonApiDataLinks(handle, type.toString(), attributeNode,
new JsonApiLinks(profileProperties.getDomain() + handle)));
}
return new JsonApiWrapperWrite(dataList);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,24 @@ void testSearchByPhysicalSpecimenId() throws Exception {
assertThat(response).isEqualTo(expected);
}

@Test
void testSearchByPhysicalSpecimenIdIsArchived() {
//Given
var handle = HANDLE.getBytes(StandardCharsets.UTF_8);
var record = List.of(new HandleAttribute(NORMALISED_SPECIMEN_OBJECT_ID, handle,
NORMALISED_PRIMARY_SPECIMEN_OBJECT_ID_TESTVAL),
new HandleAttribute(PID_STATUS, handle, "ARCHIVED"));

postAttributes(record);

// When
var response = pidRepository.searchByNormalisedPhysicalIdentifier(
List.of(NORMALISED_PRIMARY_SPECIMEN_OBJECT_ID_TESTVAL.getBytes(StandardCharsets.UTF_8)));

// Then
assertThat(response).isEmpty();
}

@Test
void testUpdateRecord() throws Exception {
// Given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@

import com.fasterxml.jackson.databind.JsonNode;
import eu.dissco.core.handlemanager.Profiles;
import eu.dissco.core.handlemanager.domain.FdoProfile;
import eu.dissco.core.handlemanager.domain.repsitoryobjects.HandleAttribute;
import eu.dissco.core.handlemanager.domain.requests.vocabulary.specimen.ObjectType;
import eu.dissco.core.handlemanager.exceptions.InvalidRequestException;
Expand Down Expand Up @@ -358,28 +359,39 @@ void testCreateDigitalSpecimen() throws Exception {
var responseReceived = service.createRecords(List.of(request));

// Then
then(pidRepository).should().postAttributesToDb(CREATED.getEpochSecond(), digitalSpecimen);
assertThat(responseReceived).isEqualTo(responseExpected);
}


@Test
void testCreateDigitalSpecimenSpecimenExists() throws Exception {
// Given
byte[] handle = handles.get(0);
var request = genCreateRecordRequest(givenDigitalSpecimenRequestObjectNullOptionals(),
var digitalSpecimen = givenDigitalSpecimenRequestObjectNullOptionals();
var request = genCreateRecordRequest(digitalSpecimen,
RECORD_TYPE_DS);
List<HandleAttribute> digitalSpecimen = genDigitalSpecimenAttributes(handle);
List<HandleAttribute> digitalSpecimenAttributes = genDigitalSpecimenAttributes(handle);
var digitalSpecimenSublist = digitalSpecimenAttributes.stream()
.filter(row -> row.getType().equals(PRIMARY_SPECIMEN_OBJECT_ID.get())).toList();
var responseExpected = givenRecordResponseWriteSmallResponse(digitalSpecimenSublist,
List.of(handle),
ObjectType.DIGITAL_SPECIMEN);

given(pidNameGeneratorService.genHandleList(1)).willReturn(new ArrayList<>(List.of(handle)));
given(pidRepository.searchByNormalisedPhysicalIdentifier(anyList())).willReturn(
digitalSpecimen);
given(profileProperties.getDomain()).willReturn(HANDLE_DOMAIN);
given(pidRepository.searchByNormalisedPhysicalIdentifier(anyList())).willReturn(List.of(
new HandleAttribute(FdoProfile.NORMALISED_SPECIMEN_OBJECT_ID, handle,
digitalSpecimen.getNormalisedPrimarySpecimenObjectId())));
given(fdoRecordService.prepareUpdateAttributes(any(), eq(request.get("data").get("attributes")),
eq(ObjectType.DIGITAL_SPECIMEN))).willReturn(digitalSpecimenAttributes);

// When
Exception e = assertThrows(InvalidRequestException.class,
() -> service.createRecords(List.of(request)));
var result = service.createRecords(List.of(request));

// Then
assertThat(e.getMessage()).contains(new String(handle, StandardCharsets.UTF_8));
then(pidRepository).should()
.updateRecordBatch(CREATED.getEpochSecond(), List.of(digitalSpecimenAttributes), true);
assertThat(result).isEqualTo(responseExpected);
}

@Test
Expand Down Expand Up @@ -424,6 +436,7 @@ void testCreateMasRecord() throws Exception {
given(profileProperties.getDomain()).willReturn(HANDLE_DOMAIN);

// When

var responseReceived = service.createRecords(List.of(request));

// Then
Expand Down
Loading