Skip to content

Commit

Permalink
MODLD-563: API for loading record to LDE (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
askhat-abishev authored Nov 1, 2024
1 parent 3c29f2c commit 9afe937
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 14 deletions.
27 changes: 27 additions & 0 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@
"pathPattern": "/resource/preview/{inventoryId}",
"permissionsRequired": [ "linked-data.resources.preview.get" ],
"modulePermissions": ["source-storage.records.get"]
},
{
"methods": [ "POST" ],
"pathPattern": "/resource/import/{inventoryId}",
"permissionsRequired": [ "linked-data.resources.import.post" ],
"modulePermissions": [
"source-storage.records.get",
"mapping-metadata.get",
"mapping-metadata.type.item.get",
"inventory-storage.instances.item.post",
"inventory-storage.instances.item.put",
"inventory-storage.instances.item.delete",
"inventory-storage.instance-types.item.post",
"inventory-storage.preceding-succeeding-titles.collection.get",
"inventory-storage.preceding-succeeding-titles.item.get",
"inventory-storage.preceding-succeeding-titles.item.post",
"inventory-storage.preceding-succeeding-titles.item.put",
"inventory-storage.preceding-succeeding-titles.item.delete",
"source-storage.snapshots.post",
"source-storage.records.post",
"source-storage.records.put"
]
}
]
},
Expand Down Expand Up @@ -190,6 +212,11 @@
"displayName": "Linked Data: Get the preview of a resource",
"description": "Get the preview of a linked-data resource"
},
{
"permissionName": "linked-data.resources.import.post",
"displayName": "Linked Data: Create a bibliographic resource derived from MARC record",
"description": "Create a bibliographic linked-data resource derived from MARC record"
},
{
"permissionName": "linked-data.profiles.get",
"displayName": "Linked Data: Get the profiles for performing CRUD operations on resources",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.folio.linked.data.controller;

import static org.springframework.http.HttpStatus.CREATED;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.folio.linked.data.domain.dto.ResourceIdDto;
Expand Down Expand Up @@ -44,6 +46,13 @@ public ResponseEntity<ResourceResponseDto> getResourcePreviewByInventoryId(Strin
return ResponseEntity.ok(resourceMarcService.getResourcePreviewByInventoryId(inventoryId));
}

@Override
public ResponseEntity<ResourceIdDto> importMarcRecord(String inventoryId) {
return ResponseEntity
.status(CREATED)
.body(resourceMarcService.importMarcRecord(inventoryId));
}

@Override
public ResponseEntity<ResourceResponseDto> updateResource(Long id, String okapiTenant,
@Valid ResourceRequestDto resourceDto) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.folio.linked.data.service.resource;

import org.folio.ld.dictionary.model.Resource;
import org.folio.linked.data.domain.dto.ResourceIdDto;
import org.folio.linked.data.domain.dto.ResourceMarcViewDto;
import org.folio.linked.data.domain.dto.ResourceResponseDto;

Expand All @@ -14,4 +15,6 @@ public interface ResourceMarcService {

ResourceResponseDto getResourcePreviewByInventoryId(String inventoryId);

ResourceIdDto importMarcRecord(String inventoryId);

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.folio.linked.data.service.resource;

import static java.lang.String.format;
import static java.util.Objects.isNull;
import static org.folio.ld.dictionary.PredicateDictionary.REPLACED_BY;
import static org.folio.ld.dictionary.PropertyDictionary.RESOURCE_PREFERRED;
import static org.folio.ld.dictionary.ResourceTypeDictionary.INSTANCE;
import static org.folio.ld.dictionary.model.ResourceSource.LINKED_DATA;
import static org.folio.linked.data.util.BibframeUtils.extractWorkFromInstance;
import static org.folio.linked.data.util.Constants.IS_NOT_FOUND;
import static org.folio.linked.data.util.Constants.RESOURCE_WITH_GIVEN_ID;
Expand All @@ -12,6 +14,7 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import feign.FeignException;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
Expand All @@ -20,8 +23,10 @@
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.folio.linked.data.client.SrsClient;
import org.folio.linked.data.domain.dto.ResourceIdDto;
import org.folio.linked.data.domain.dto.ResourceMarcViewDto;
import org.folio.linked.data.domain.dto.ResourceResponseDto;
import org.folio.linked.data.exception.AlreadyExistsException;
import org.folio.linked.data.exception.NotFoundException;
import org.folio.linked.data.exception.ValidationException;
import org.folio.linked.data.mapper.ResourceModelMapper;
Expand All @@ -41,6 +46,8 @@
import org.folio.rest.jaxrs.model.ParsedRecord;
import org.folio.rest.jaxrs.model.Record;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -50,6 +57,9 @@
@RequiredArgsConstructor
public class ResourceMarcServiceImpl implements ResourceMarcService {

private static final String RESOURCE_NOT_FOUND_BY_SRS_ID = "Resource not found by srsId: ";
private static final String RECORD_NOT_FOUND_BY_INVENTORY_ID = "Record with inventoryId: %s was not found";

private final ObjectMapper objectMapper;
private final ResourceRepository resourceRepo;
private final ResourceEdgeRepository edgeRepo;
Expand Down Expand Up @@ -78,7 +88,7 @@ public Long saveMarcResource(org.folio.ld.dictionary.model.Resource modelResourc
@Transactional(readOnly = true)
public ResourceMarcViewDto getResourceMarcView(Long id) {
var resource = resourceRepo.findById(id)
.orElseThrow(() -> new NotFoundException(RESOURCE_WITH_GIVEN_ID + id + IS_NOT_FOUND));
.orElseThrow(() -> createNotFoundException(RESOURCE_WITH_GIVEN_ID + id + IS_NOT_FOUND));
validateMarkViewSupportedType(resource);
var resourceModel = resourceModelMapper.toModel(resource);
var marc = bibframe2MarcMapper.toMarcJson(resourceModel);
Expand All @@ -87,34 +97,44 @@ public ResourceMarcViewDto getResourceMarcView(Long id) {

@Override
public Boolean isSupportedByInventoryId(String inventoryId) {
var response = srsClient.getFormattedSourceStorageInstanceRecordById(inventoryId);
return Optional.ofNullable(response.getBody())
return getRecord(inventoryId)
.map(HttpEntity::getBody)
.map(Record::getParsedRecord)
.map(parsedRecord -> (Map<?, ?>) parsedRecord.getContent())
.map(content -> (String) content.get("leader"))
.map(this::isMonograph)
.orElse(false);
.orElseThrow(() -> createNotFoundException(format(RECORD_NOT_FOUND_BY_INVENTORY_ID, inventoryId)));
}

@Override
public ResourceResponseDto getResourcePreviewByInventoryId(String inventoryId) {
var response = srsClient.getFormattedSourceStorageInstanceRecordById(inventoryId);
return Optional.ofNullable(response.getBody())
.map(Record::getParsedRecord)
.map(ParsedRecord::getContent)
.map(this::toJsonString)
.flatMap(marcBib2ldMapper::fromMarcJson)
return getResource(inventoryId)
.map(resourceModelMapper::toEntity)
.map(resourceDtoMapper::toDto)
.orElse(null);
.orElseThrow(() -> createNotFoundException(format(RECORD_NOT_FOUND_BY_INVENTORY_ID, inventoryId)));
}

@Override
public ResourceIdDto importMarcRecord(String inventoryId) {
return getResource(inventoryId)
.map(resource -> {
resource.getFolioMetadata().setSource(LINKED_DATA);
return resource;
})
.map(this::save)
.map(String::valueOf)
.map(id -> new ResourceIdDto().id(id))
.orElseThrow(() -> createNotFoundException(format(RECORD_NOT_FOUND_BY_INVENTORY_ID, inventoryId)));
}

private void validateMarkViewSupportedType(Resource resource) {
if (resource.isOfType(INSTANCE)) {
return;
}
var message = "Resource is not supported for MARC view";
log.error(message);
throw new ValidationException(
"Resource is not supported for MARC view", "type",
message, "type",
resource.getTypes().stream()
.map(ResourceTypeEntity::getUri)
.collect(Collectors.joining(", ", "[", "]"))
Expand Down Expand Up @@ -146,7 +166,7 @@ private Long replaceAuthority(Resource resource) {
"be saved as a new version of previously existed resource [id " + previous.getId() + "]");
return saveAndPublishEvent(resource, saved -> new ResourceReplacedEvent(previousObsolete, saved));
})
.orElseThrow(() -> new NotFoundException("Resource not found by srsId: " + srsId));
.orElseThrow(() -> createNotFoundException(RESOURCE_NOT_FOUND_BY_SRS_ID + srsId));
}

private Resource markObsolete(Resource resource) {
Expand All @@ -173,7 +193,7 @@ private Long replaceBibliographic(Resource resource) {
"replace previously existed [id " + existedBySrsId.getId() + "]");
return saveAndPublishEvent(resource, saved -> new ResourceReplacedEvent(existedBySrsId, saved));
})
.orElseThrow(() -> new NotFoundException("Resource not found by srsId: " + srsId));
.orElseThrow(() -> createNotFoundException(RESOURCE_NOT_FOUND_BY_SRS_ID + srsId));
}

private Long updateResource(Resource resource) {
Expand Down Expand Up @@ -230,8 +250,52 @@ private boolean isMonograph(String leader) {
return isLanguageMaterial(leader.charAt(6)) && isMonographicComponentPartOrItem(leader.charAt(7));
}

private Optional<ResponseEntity<Record>> getRecord(String inventoryId) {
try {
return Optional.of(srsClient.getFormattedSourceStorageInstanceRecordById(inventoryId));
} catch (FeignException.NotFound e) {
return Optional.empty();
}
}

private Optional<org.folio.ld.dictionary.model.Resource> getResource(String inventoryId) {
return getRecord(inventoryId)
.map(HttpEntity::getBody)
.map(Record::getParsedRecord)
.map(ParsedRecord::getContent)
.map(this::toJsonString)
.flatMap(marcBib2ldMapper::fromMarcJson);
}

private Long save(org.folio.ld.dictionary.model.Resource modelResource) {
var id = modelResource.getId();
if (resourceRepo.existsById(id)) {
var message = format("Another resource with ID: %s already exists in the graph", id);
log.error(message);
throw new AlreadyExistsException(message);
}

var srsId = modelResource.getFolioMetadata().getSrsId();
if (folioMetadataRepository.existsBySrsId(srsId)) {
var message = format("MARC record having srsID: %s is already imported to data graph", srsId);
log.error(message);
throw new AlreadyExistsException(message);
}

// Emitting a ResourceCreatedEvent will send a "CREATE_INSTANCE" Kafka event to Inventory,
// resulting in a duplicate instance record in Inventory. By emitting a ResourceUpdatedEvent instead,
// we send an "UPDATE_INSTANCE" event, switching the source of the existing instance
// from "MARC" to "LINKED_DATA" in Inventory.
return saveAndPublishEvent(resourceModelMapper.toEntity(modelResource), ResourceUpdatedEvent::new);
}

@SneakyThrows
private String toJsonString(Object content) {
return objectMapper.writeValueAsString(content);
}

private NotFoundException createNotFoundException(String message) {
log.error(message);
return new NotFoundException(message);
}
}
24 changes: 24 additions & 0 deletions src/main/resources/swagger.api/mod-linked-data.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ paths:
schema:
type: string
example: true|false
'404':
description: No Record is found by inventory id
'500':
$ref: '#/components/responses/internalServerErrorResponse'

Expand All @@ -170,6 +172,28 @@ paths:
application/json:
schema:
$ref: schema/resourceResponseDto.json
'404':
description: No Record is found by inventory id
'500':
$ref: '#/components/responses/internalServerErrorResponse'

/resource/import/{inventoryId}:
post:
operationId: importMarcRecord
description: Create a Resource derived from MARC record
parameters:
- $ref: '#/components/parameters/inventoryId'
responses:
'201':
description: Json object with id of a resource
content:
application/json:
schema:
$ref: schema/resourceIdDto.json
'400':
$ref: '#/components/responses/badRequestResponse'
'404':
description: No Record is found by inventory id
'500':
$ref: '#/components/responses/internalServerErrorResponse'

Expand Down
Loading

0 comments on commit 9afe937

Please sign in to comment.