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
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
- Remove AUTO_SAVE_MARC_BIB_AS_GRAPH [MODLD-1025](https://folio-org.atlassian.net/browse/MODLD-1025)
- Preserve user context and headers in Kafka processing. Remove all references to system user [MODLD-1031](https://folio-org.atlassian.net/browse/MODLD-1031)
- Make profile settings transactional to address inconsistency bug [MODLD-1030](https://folio-org.atlassian.net/browse/MODLD-1030)
- Analytical Entries added to Work Response DTO (MODLD-1029)[https://folio-org.atlassian.net/browse/MODLD-1029]

## 1.0.4 (04-24-2025)
- Work Edit form - Instance read-only section: "Notes about the instance" data is not shown [MODLD-716](https://folio-org.atlassian.net/browse/MODLD-716)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import static org.folio.linked.data.util.ResourceUtils.getTypeUris;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -56,15 +57,13 @@ public <P> Resource toEntity(@NonNull Object dto, @NonNull Class<P> parentReques

@Override
public <D> D toDto(@NonNull Resource source, @NonNull D parentDto, Resource parentResource, Predicate predicate) {
// Of all the types of the resource, take the first one that has a mapper
var resourceMapper = source.getTypes()
return source.getTypes()
.stream()
.map(type -> getMapperUnit(type.getUri(), predicate, parentDto.getClass(), null))
.flatMap(Optional::stream)
.findFirst();

return resourceMapper
.map(mapper -> mapper.toDto(source, parentDto, new ResourceMappingContext(parentResource, predicate)))
.filter(Objects::nonNull)
.findFirst()
.orElseGet(() -> {
var types = String.join(", ", getTypeUris(source));
log.debug(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import java.util.Set;
import org.folio.ld.dictionary.model.Predicate;
import org.folio.linked.data.model.entity.Resource;
import org.jspecify.annotations.Nullable;

public interface SingleResourceMapperUnit {

@Nullable
<P> P toDto(Resource resourceToConvert, P parentDto, ResourceMappingContext context);

Resource toEntity(Object dto, Resource parentEntity);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.folio.linked.data.mapper.dto.resource.common.work.sub;

import static org.folio.ld.dictionary.PredicateDictionary.IS_PART_OF;
import static org.folio.ld.dictionary.PredicateDictionary.OTHER_EDITION;
import static org.folio.ld.dictionary.PredicateDictionary.OTHER_VERSION;
import static org.folio.ld.dictionary.PredicateDictionary.RELATED_WORK;
import static org.folio.ld.dictionary.PropertyDictionary.LABEL;
import static org.folio.ld.dictionary.ResourceTypeDictionary.LIGHT_RESOURCE;
import static org.folio.ld.dictionary.ResourceTypeDictionary.SERIES;
import static org.folio.linked.data.util.ResourceUtils.getFirstPropertyValue;

import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.folio.linked.data.domain.dto.LightWork;
import org.folio.linked.data.domain.dto.WorkResponse;
import org.folio.linked.data.exception.RequestProcessingExceptionBuilder;
import org.folio.linked.data.mapper.dto.resource.base.CoreMapper;
import org.folio.linked.data.mapper.dto.resource.base.MapperUnit;
import org.folio.linked.data.mapper.dto.resource.base.SingleResourceMapperUnit;
import org.folio.linked.data.model.entity.Resource;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@MapperUnit(type = LIGHT_RESOURCE, predicate = {IS_PART_OF, OTHER_EDITION, OTHER_VERSION, RELATED_WORK},
requestDto = LightWork.class)
public class LightWorkMapperUnit implements SingleResourceMapperUnit {

private static final Set<Class<?>> SUPPORTED_PARENTS = Set.of(
WorkResponse.class
);
private final CoreMapper coreMapper;
private final RequestProcessingExceptionBuilder exceptionBuilder;

@Override
public Set<Class<?>> supportedParents() {
return SUPPORTED_PARENTS;
}

@Override
public <P> P toDto(Resource resourceToConvert, P parentDto, ResourceMappingContext context) {
if (resourceToConvert.isOfType(SERIES)) {
return null;
}
if (parentDto instanceof WorkResponse workResponse) {
var lightWork = coreMapper.toDtoWithEdges(resourceToConvert, LightWork.class, false);
lightWork.setId(String.valueOf(resourceToConvert.getId()));
lightWork.setLabel(getFirstPropertyValue(resourceToConvert, LABEL));
lightWork.setRelation(context.predicate().getUri());
workResponse.addAnalyticalEntryItem(lightWork);
}
return parentDto;
}

@Override
public Resource toEntity(Object dto, Resource parentEntity) {
throw exceptionBuilder.notSupportedException(LIGHT_RESOURCE.name(), "Create or update");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

import static org.folio.ld.dictionary.PredicateDictionary.DISSERTATION;
import static org.folio.ld.dictionary.PredicateDictionary.GENRE;
import static org.folio.ld.dictionary.PredicateDictionary.IS_PART_OF;
import static org.folio.ld.dictionary.PredicateDictionary.OTHER_EDITION;
import static org.folio.ld.dictionary.PredicateDictionary.OTHER_VERSION;
import static org.folio.ld.dictionary.PredicateDictionary.RELATED_WORK;
import static org.folio.ld.dictionary.ResourceTypeDictionary.SERIES;
import static org.folio.ld.dictionary.ResourceTypeDictionary.WORK;

import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.folio.ld.dictionary.PredicateDictionary;
Expand All @@ -23,8 +29,16 @@
@RequiredArgsConstructor
public class ResourceEdgeServiceImpl implements ResourceEdgeService {

private static final Map<ResourceTypeDictionary, Set<PredicateDictionary>> OUTGOING_EDGES_TO_BE_COPIED = Map.of(
WORK, Set.of(DISSERTATION, GENRE)
private static final Map<ResourceTypeDictionary, Map<PredicateDictionary, Predicate<ResourceEdge>>>
PARENT_TO_OUTGOING_EDGE_AND_CONDITION = Map.of(
WORK, Map.of(
DISSERTATION, edge -> true,
GENRE, edge -> true,
IS_PART_OF, edge -> !edge.getTarget().isOfType(SERIES),
OTHER_EDITION, edge -> true,
OTHER_VERSION, edge -> true,
RELATED_WORK, edge -> true
)
);
private final ResourceModelMapper resourceModelMapper;
private final ResourceEdgeRepository resourceEdgeRepository;
Expand Down Expand Up @@ -68,21 +82,13 @@ public ResourceEdgePk saveNewResourceEdge(Long sourceId,
}

private Set<ResourceEdge> getOutgoingEdgesToBeCopied(Resource resource) {
var predicatesToBeCopied = getPredicatesToBeCopied(resource);
return getOutgoingEdgesWithPredicate(resource, predicatesToBeCopied);
}

private Set<ResourceEdge> getOutgoingEdgesWithPredicate(Resource resource, Set<String> predicateUris) {
return resource.getOutgoingEdges().stream()
.filter(edge -> predicateUris.contains(edge.getPredicate().getUri()))
.collect(Collectors.toSet());
}

private Set<String> getPredicatesToBeCopied(Resource resource) {
return OUTGOING_EDGES_TO_BE_COPIED.entrySet().stream()
return PARENT_TO_OUTGOING_EDGE_AND_CONDITION.entrySet().stream()
.filter(entry -> resource.isOfType(entry.getKey()))
.flatMap(entry -> entry.getValue().stream())
.map(PredicateDictionary::getUri)
.flatMap(entry -> resource.getOutgoingEdges().stream()
.filter(edge -> PredicateDictionary.fromUri(edge.getPredicate().getUri())
.map(entry.getValue()::get)
.map(condition -> condition.test(edge))
.orElse(false)))
.collect(Collectors.toSet());
}

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/folio/linked/data/util/ResourceUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ public static List<String> getTypeUris(Resource resource) {
.toList();
}

public static String getFirstPropertyValue(Resource resource, PropertyDictionary property) {
return getPropertyValues(resource, property)
.getFirst();
}

public static List<String> getPropertyValues(Resource resource, PropertyDictionary property) {
return ofNullable(resource.getDoc())
.map(doc -> doc.get(property.getValue()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.apache.commons.collections4.CollectionUtils.isEmpty;
import static org.folio.ld.dictionary.PropertyDictionary.MAIN_TITLE;
import static org.folio.ld.dictionary.ResourceTypeDictionary.INSTANCE;
import static org.folio.ld.dictionary.ResourceTypeDictionary.LIGHT_RESOURCE;
import static org.folio.ld.dictionary.ResourceTypeDictionary.SERIES;
import static org.folio.ld.dictionary.ResourceTypeDictionary.TITLE;
import static org.folio.ld.dictionary.ResourceTypeDictionary.WORK;
Expand All @@ -20,7 +21,7 @@ public class PrimaryTitleEntityValidator implements ConstraintValidator<PrimaryT

@Override
public boolean isValid(Resource resource, ConstraintValidatorContext context) {
if (isNotWorkOrInstance(resource) || isSeries(resource)) {
if (isNotWorkOrInstance(resource) || isSeries(resource) || isLightResource(resource)) {
return true;
}
if (isEmpty(resource.getOutgoingEdges())) {
Expand All @@ -45,4 +46,8 @@ private boolean isNotWorkOrInstance(Resource resource) {
private boolean isSeries(Resource resource) {
return resource.isOfType(SERIES);
}

private boolean isLightResource(Resource resource) {
return resource.isOfType(LIGHT_RESOURCE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Light Work",
"allOf": [
{
"$ref": "IdField.json"
},
{
"type": "object",
"properties": {
"label": {
"type": "string"
},
"_relation": {
"type": "string"
}
}
}
]
}

Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@
"type": "object",
"$ref": "../common/HubReferenceWithType.json"
}
},
"_analyticalEntry": {
"type": "array",
"items": {
"type": "object",
"$ref": "../common/LightWork.json"
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package org.folio.linked.data.e2e.mappings.work.lightwork;

import static org.folio.ld.dictionary.PredicateDictionary.IS_PART_OF;
import static org.folio.ld.dictionary.PredicateDictionary.OTHER_EDITION;
import static org.folio.ld.dictionary.PredicateDictionary.OTHER_VERSION;
import static org.folio.ld.dictionary.PredicateDictionary.RELATED_WORK;
import static org.folio.ld.dictionary.PropertyDictionary.LABEL;
import static org.folio.ld.dictionary.ResourceTypeDictionary.LIGHT_RESOURCE;
import static org.folio.ld.dictionary.ResourceTypeDictionary.SERIES;
import static org.folio.ld.dictionary.ResourceTypeDictionary.WORK;
import static org.folio.linked.data.test.TestUtil.STANDALONE_TEST_PROFILE;
import static org.folio.linked.data.test.TestUtil.TEST_JSON_MAPPER;
import static org.folio.linked.data.test.TestUtil.defaultHeaders;
import static org.folio.linked.data.util.Constants.STANDALONE_PROFILE;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import lombok.SneakyThrows;
import org.folio.ld.dictionary.PredicateDictionary;
import org.folio.linked.data.e2e.base.ITBase;
import org.folio.linked.data.e2e.base.IntegrationTest;
import org.folio.linked.data.model.entity.Resource;
import org.folio.linked.data.model.entity.ResourceEdge;
import org.folio.linked.data.test.MonographTestUtil;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;

@IntegrationTest
@ActiveProfiles({STANDALONE_PROFILE, STANDALONE_TEST_PROFILE})
class LightWorkIT extends ITBase {

private static final String RESOURCE_URL = "/linked-data/resource";
private static final Long IS_PART_OF_ID = 200L;
private static final Long OTHER_EDITION_ID = 201L;
private static final Long OTHER_VERSION_ID = 202L;
private static final Long RELATED_WORK_ID = 203L;

@Test
@SneakyThrows
void getWork_withLightWorkEdges_shouldReturnAnalyticalEntryForEachRelation() {
// given
var work = buildWorkWithLightWorkEdges();
resourceTestService.saveGraph(work);
var getRequest = get(RESOURCE_URL + "/" + work.getId())
.contentType(APPLICATION_JSON)
.headers(defaultHeaders(env));

// when
var response = mockMvc.perform(getRequest);

// then
var analyticalEntryPath = "$.resource['http://bibfra.me/vocab/lite/Work']['_analyticalEntry']";
response
.andExpect(status().isOk())
.andExpect(jsonPath(analyticalEntryPath, hasSize(4)))
.andExpect(jsonPath(analyticalEntryPath + "[*]['id']", containsInAnyOrder(
IS_PART_OF_ID.toString(),
OTHER_EDITION_ID.toString(),
OTHER_VERSION_ID.toString(),
RELATED_WORK_ID.toString()
)))
.andExpect(jsonPath(analyticalEntryPath + "[*]['_relation']", containsInAnyOrder(
IS_PART_OF.getUri(),
OTHER_EDITION.getUri(),
OTHER_VERSION.getUri(),
RELATED_WORK.getUri()
)));
}

@Test
@SneakyThrows
void getWork_withLightWorkEdgesAndPartOfSeries_shouldReturnCorrectAnalyticalEntries() {
// given
var work = buildWorkWithLightWorkEdges();
var series = new Resource().setLabel("Series").addTypes(WORK, SERIES, LIGHT_RESOURCE).setIdAndRefreshEdges(333L);
work.addOutgoingEdge(new ResourceEdge(work, series, IS_PART_OF));
resourceTestService.saveGraph(work);
var getRequest = get(RESOURCE_URL + "/" + work.getId())
.contentType(APPLICATION_JSON)
.headers(defaultHeaders(env));

// when
var response = mockMvc.perform(getRequest);

// then
var analyticalEntryPath = "$.resource['http://bibfra.me/vocab/lite/Work']['_analyticalEntry']";
response
.andExpect(status().isOk())
.andExpect(jsonPath(analyticalEntryPath, hasSize(4)))
.andExpect(jsonPath(analyticalEntryPath + "[*]['id']", containsInAnyOrder(
IS_PART_OF_ID.toString(),
OTHER_EDITION_ID.toString(),
OTHER_VERSION_ID.toString(),
RELATED_WORK_ID.toString()
)))
.andExpect(jsonPath(analyticalEntryPath + "[*]['_relation']", containsInAnyOrder(
IS_PART_OF.getUri(),
OTHER_EDITION.getUri(),
OTHER_VERSION.getUri(),
RELATED_WORK.getUri()
)));
}

@SneakyThrows
private Resource buildWorkWithLightWorkEdges() {
var work = MonographTestUtil.getWork("work", hashService);
addLightWorkEdge(work, IS_PART_OF_ID, IS_PART_OF);
addLightWorkEdge(work, OTHER_EDITION_ID, OTHER_EDITION);
addLightWorkEdge(work, OTHER_VERSION_ID, OTHER_VERSION);
addLightWorkEdge(work, RELATED_WORK_ID, RELATED_WORK);
return work;
}

@SneakyThrows
private void addLightWorkEdge(Resource work, Long lightWorkId, PredicateDictionary predicate) {
var doc = TEST_JSON_MAPPER.readTree("""
{"%s": ["%s"]}""".formatted(LABEL.getValue(), labelFor(lightWorkId)));
var lightWork = new Resource()
.addTypes(LIGHT_RESOURCE, WORK)
.setDoc(doc)
.setLabel(labelFor(lightWorkId))
.setIdAndRefreshEdges(lightWorkId);
work.addOutgoingEdge(new ResourceEdge(work, lightWork, predicate));
}

private String labelFor(Long id) {
return "light work label " + id;
}
}

Loading
Loading