Skip to content

Commit 4ec9b37

Browse files
committed
MODLD-1029: lightWork label fix; modelMapper maps incoming edges too
1 parent fe86dad commit 4ec9b37

4 files changed

Lines changed: 186 additions & 52 deletions

File tree

src/main/java/org/folio/linked/data/mapper/ResourceModelMapper.java

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313
import java.util.Map;
1414
import java.util.Objects;
1515
import java.util.Set;
16+
import java.util.function.BiFunction;
17+
import java.util.function.Supplier;
1618
import org.folio.ld.dictionary.PredicateDictionary;
1719
import org.folio.ld.dictionary.ResourceTypeDictionary;
1820
import org.folio.linked.data.model.entity.FolioMetadata;
1921
import org.folio.linked.data.model.entity.PredicateEntity;
2022
import org.folio.linked.data.model.entity.Resource;
23+
import org.folio.linked.data.model.entity.ResourceEdge;
2124
import org.folio.linked.data.model.entity.ResourceTypeEntity;
2225
import org.mapstruct.AfterMapping;
2326
import org.mapstruct.BeforeMapping;
@@ -58,16 +61,16 @@ public org.folio.ld.dictionary.model.Resource toModel(Resource entity) {
5861
}
5962

6063
@NotForGeneration
61-
public org.folio.ld.dictionary.model.Resource toModel(Resource entity, int outgoingEdgesDepth) {
62-
if (outgoingEdgesDepth > MAX_ENTITY_TO_MODEL_EDGE_DEPTH) {
63-
throw new IllegalArgumentException("Requested outgoing edges depth is too high: " + outgoingEdgesDepth
64+
public org.folio.ld.dictionary.model.Resource toModel(Resource entity, int maxEdgesDepth) {
65+
if (maxEdgesDepth > MAX_ENTITY_TO_MODEL_EDGE_DEPTH) {
66+
throw new IllegalArgumentException("Requested edges depth is too high: " + maxEdgesDepth
6467
+ ". Maximum allowed is " + MAX_ENTITY_TO_MODEL_EDGE_DEPTH);
6568
}
66-
return toModel(entity, new CyclicGraphContext(), new DepthContext(outgoingEdgesDepth));
69+
return toModel(entity, new CyclicGraphContext(), new DepthContext(maxEdgesDepth, maxEdgesDepth));
6770
}
6871

69-
@Mapping(ignore = true, target = "incomingEdges")
7072
@Mapping(ignore = true, target = "outgoingEdges")
73+
@Mapping(ignore = true, target = "incomingEdges")
7174
protected abstract org.folio.ld.dictionary.model.Resource toModel(Resource entity,
7275
@Context CyclicGraphContext cycleContext,
7376
@Context DepthContext depthContext);
@@ -77,18 +80,37 @@ protected void mapOutgoingEdges(@MappingTarget org.folio.ld.dictionary.model.Res
7780
Resource source,
7881
@Context CyclicGraphContext cycleContext,
7982
@Context DepthContext depthContext) {
80-
if (!depthContext.allowsEdges()) {
81-
return;
83+
target.setOutgoingEdges(mapEdges(source.getOutgoingEdges(), depthContext.outgoingAllowsEdges(),
84+
depthContext::nextOutgoing, (edge, next) ->
85+
mapEdge(target, toModel(edge.getTarget(), cycleContext, next), edge.getPredicate())));
86+
}
87+
88+
@AfterMapping
89+
protected void mapIncomingEdges(@MappingTarget org.folio.ld.dictionary.model.Resource target,
90+
Resource source,
91+
@Context CyclicGraphContext cycleContext,
92+
@Context DepthContext depthContext) {
93+
target.setIncomingEdges(mapEdges(source.getIncomingEdges(), depthContext.incomingAllowsEdges(),
94+
depthContext::nextIncoming, (edge, next) ->
95+
mapEdge(toModel(edge.getSource(), cycleContext, next), target, edge.getPredicate())));
96+
}
97+
98+
private LinkedHashSet<org.folio.ld.dictionary.model.ResourceEdge> mapEdges(
99+
Set<ResourceEdge> edges,
100+
boolean allowsEdges,
101+
Supplier<DepthContext> nextDepth,
102+
BiFunction<ResourceEdge, DepthContext, org.folio.ld.dictionary.model.ResourceEdge> edgeMapper) {
103+
if (!allowsEdges) {
104+
return new LinkedHashSet<>();
82105
}
106+
var next = nextDepth.get();
107+
return edges.stream().map(edge -> edgeMapper.apply(edge, next)).collect(toCollection(LinkedHashSet::new));
108+
}
83109

84-
var nextDepth = depthContext.nextDepth();
85-
var outgoingEdges = source.getOutgoingEdges().stream()
86-
.map(edge -> new org.folio.ld.dictionary.model.ResourceEdge(
87-
target,
88-
toModel(edge.getTarget(), cycleContext, nextDepth),
89-
map(edge.getPredicate())))
90-
.collect(toCollection(LinkedHashSet::new));
91-
target.setOutgoingEdges(outgoingEdges);
110+
private org.folio.ld.dictionary.model.ResourceEdge mapEdge(org.folio.ld.dictionary.model.Resource edgeSource,
111+
org.folio.ld.dictionary.model.Resource edgeTarget,
112+
PredicateEntity predicate) {
113+
return new org.folio.ld.dictionary.model.ResourceEdge(edgeSource, edgeTarget, map(predicate));
92114
}
93115

94116
protected Set<ResourceTypeDictionary> map(Set<ResourceTypeEntity> typeEntities) {
@@ -145,18 +167,28 @@ protected void storeMappedEntity(org.folio.ld.dictionary.model.Resource source,
145167
}
146168

147169
protected static final class DepthContext {
148-
private final int depth;
170+
private final int outgoingDepth;
171+
private final int incomingDepth;
172+
173+
private DepthContext(int outgoingDepth, int incomingDepth) {
174+
this.outgoingDepth = Math.max(outgoingDepth, 0);
175+
this.incomingDepth = Math.max(incomingDepth, 0);
176+
}
177+
178+
private boolean outgoingAllowsEdges() {
179+
return outgoingDepth > 0;
180+
}
149181

150-
private DepthContext(int depth) {
151-
this.depth = Math.max(depth, 0);
182+
private boolean incomingAllowsEdges() {
183+
return incomingDepth > 0;
152184
}
153185

154-
private boolean allowsEdges() {
155-
return depth > 0;
186+
private DepthContext nextOutgoing() {
187+
return new DepthContext(outgoingDepth - 1, incomingDepth);
156188
}
157189

158-
private DepthContext nextDepth() {
159-
return depth <= 0 ? this : new DepthContext(depth - 1);
190+
private DepthContext nextIncoming() {
191+
return new DepthContext(outgoingDepth, incomingDepth - 1);
160192
}
161193
}
162194

src/main/java/org/folio/linked/data/mapper/dto/resource/common/work/sub/LightWorkMapperUnit.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.folio.linked.data.mapper.dto.resource.common.work.sub;
22

3+
import static org.folio.ld.dictionary.PredicateDictionary.CREATOR;
34
import static org.folio.ld.dictionary.PredicateDictionary.IS_PART_OF;
45
import static org.folio.ld.dictionary.PredicateDictionary.OTHER_EDITION;
56
import static org.folio.ld.dictionary.PredicateDictionary.OTHER_VERSION;
@@ -18,6 +19,7 @@
1819
import org.folio.linked.data.mapper.dto.resource.base.MapperUnit;
1920
import org.folio.linked.data.mapper.dto.resource.base.SingleResourceMapperUnit;
2021
import org.folio.linked.data.model.entity.Resource;
22+
import org.folio.linked.data.model.entity.ResourceEdge;
2123
import org.springframework.stereotype.Component;
2224

2325
@Component
@@ -45,13 +47,25 @@ public <P> P toDto(Resource resourceToConvert, P parentDto, ResourceMappingConte
4547
if (parentDto instanceof WorkResponse workResponse) {
4648
var lightWork = coreMapper.toDtoWithEdges(resourceToConvert, LightWork.class, false);
4749
lightWork.setId(String.valueOf(resourceToConvert.getId()));
48-
lightWork.setLabel(getFirstPropertyValue(resourceToConvert, LABEL));
50+
lightWork.setLabel(constructUiLabel(resourceToConvert));
4951
lightWork.setRelation(context.predicate().getUri());
5052
workResponse.addAnalyticalEntryItem(lightWork);
5153
}
5254
return parentDto;
5355
}
5456

57+
private static String constructUiLabel(Resource lightWork) {
58+
var workLabel = getFirstPropertyValue(lightWork, LABEL);
59+
var creatorLabel = lightWork.getOutgoingEdges()
60+
.stream()
61+
.filter(re -> re.getPredicate().getUri().equals(CREATOR.getUri()))
62+
.map(ResourceEdge::getTarget)
63+
.map(creator -> getFirstPropertyValue(creator, LABEL))
64+
.findFirst()
65+
.orElse("");
66+
return workLabel + ". " + creatorLabel;
67+
}
68+
5569
@Override
5670
public Resource toEntity(Object dto, Resource parentEntity) {
5771
throw exceptionBuilder.notSupportedException(LIGHT_RESOURCE.name(), "Create or update");

src/test/java/org/folio/linked/data/e2e/mappings/work/lightwork/LightWorkIT.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.folio.linked.data.e2e.mappings.work.lightwork;
22

3+
import static org.folio.ld.dictionary.PredicateDictionary.CREATOR;
34
import static org.folio.ld.dictionary.PredicateDictionary.IS_PART_OF;
45
import static org.folio.ld.dictionary.PredicateDictionary.OTHER_EDITION;
56
import static org.folio.ld.dictionary.PredicateDictionary.OTHER_VERSION;
@@ -38,6 +39,7 @@ class LightWorkIT extends ITBase {
3839
private static final Long OTHER_EDITION_ID = 201L;
3940
private static final Long OTHER_VERSION_ID = 202L;
4041
private static final Long RELATED_WORK_ID = 203L;
42+
private static final Long CREATOR_ID = 204L;
4143

4244
@Test
4345
@SneakyThrows
@@ -68,9 +70,53 @@ void getWork_withLightWorkEdges_shouldReturnAnalyticalEntryForEachRelation() {
6870
OTHER_EDITION.getUri(),
6971
OTHER_VERSION.getUri(),
7072
RELATED_WORK.getUri()
73+
)))
74+
.andExpect(jsonPath(analyticalEntryPath + "[*]['label']", containsInAnyOrder(
75+
labelFor(IS_PART_OF_ID) + ". ",
76+
labelFor(OTHER_EDITION_ID) + ". ",
77+
labelFor(OTHER_VERSION_ID) + ". ",
78+
labelFor(RELATED_WORK_ID) + ". "
7179
)));
7280
}
7381

82+
@Test
83+
@SneakyThrows
84+
void getWork_withLightWorkEdgeAndCreator_shouldConstructLabelFromWorkAndCreator() {
85+
// given
86+
var work = MonographTestUtil.getWork("work", hashService);
87+
var creator = new Resource()
88+
.addTypes(LIGHT_RESOURCE)
89+
.setDoc(TEST_JSON_MAPPER.readTree("""
90+
{"%s": ["%s"]}""".formatted(LABEL.getValue(), "Creator Name")))
91+
.setLabel("Creator Name")
92+
.setIdAndRefreshEdges(CREATOR_ID);
93+
var lightWork = new Resource()
94+
.addTypes(LIGHT_RESOURCE, WORK)
95+
.setDoc(TEST_JSON_MAPPER.readTree("""
96+
{"%s": ["%s"]}""".formatted(LABEL.getValue(), labelFor(IS_PART_OF_ID))))
97+
.setLabel(labelFor(IS_PART_OF_ID))
98+
.setIdAndRefreshEdges(IS_PART_OF_ID);
99+
var creatorEdge = new ResourceEdge(lightWork, creator, CREATOR);
100+
lightWork.addOutgoingEdge(creatorEdge);
101+
creator.addIncomingEdge(creatorEdge);
102+
work.addOutgoingEdge(new ResourceEdge(work, lightWork, IS_PART_OF));
103+
resourceTestService.saveGraph(work);
104+
var getRequest = get(RESOURCE_URL + "/" + work.getId())
105+
.contentType(APPLICATION_JSON)
106+
.headers(defaultHeaders(env));
107+
108+
// when
109+
var response = mockMvc.perform(getRequest);
110+
111+
// then
112+
var analyticalEntryPath = "$.resource['http://bibfra.me/vocab/lite/Work']['_analyticalEntry']";
113+
response
114+
.andExpect(status().isOk())
115+
.andExpect(jsonPath(analyticalEntryPath, hasSize(1)))
116+
.andExpect(jsonPath(analyticalEntryPath + "[0]['label']")
117+
.value(labelFor(IS_PART_OF_ID) + ". " + "Creator Name"));
118+
}
119+
74120
@Test
75121
@SneakyThrows
76122
void getWork_withLightWorkEdgesAndPartOfSeries_shouldReturnCorrectAnalyticalEntries() {
@@ -102,6 +148,12 @@ void getWork_withLightWorkEdgesAndPartOfSeries_shouldReturnCorrectAnalyticalEntr
102148
OTHER_EDITION.getUri(),
103149
OTHER_VERSION.getUri(),
104150
RELATED_WORK.getUri()
151+
)))
152+
.andExpect(jsonPath(analyticalEntryPath + "[*]['label']", containsInAnyOrder(
153+
labelFor(IS_PART_OF_ID) + ". ",
154+
labelFor(OTHER_EDITION_ID) + ". ",
155+
labelFor(OTHER_VERSION_ID) + ". ",
156+
labelFor(RELATED_WORK_ID) + ". "
105157
)));
106158
}
107159

src/test/java/org/folio/linked/data/mapper/ResourceModelMapperTest.java

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,79 +2,114 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
import static org.folio.ld.dictionary.PredicateDictionary.TITLE;
56

6-
import org.folio.ld.dictionary.PredicateDictionary;
77
import org.folio.ld.dictionary.ResourceTypeDictionary;
88
import org.folio.linked.data.model.entity.Resource;
99
import org.folio.linked.data.model.entity.ResourceEdge;
1010
import org.folio.spring.testing.type.UnitTest;
1111
import org.junit.jupiter.api.Test;
12+
import org.junit.jupiter.params.ParameterizedTest;
13+
import org.junit.jupiter.params.provider.CsvSource;
1214

1315
@UnitTest
1416
class ResourceModelMapperTest {
1517

1618
private final ResourceModelMapper mapper = new ResourceModelMapperImpl();
1719

18-
@Test
19-
void toModel_depthZero_shouldNotMapOutgoingEdges() {
20+
@ParameterizedTest
21+
@CsvSource({
22+
"0, 0, 0",
23+
"1, 1, 0",
24+
"2, 1, 1"
25+
})
26+
void toModel_shouldMapEdgesUpToRequestedDepth(int depth, int expectedLevel1Size, int expectedLevel2Size) {
2027
// given
21-
var root = buildThreeNodeGraph();
28+
var root = resource(1L);
29+
var child = resource(2L);
30+
var grandChild = resource(3L);
31+
var parent = resource(4L);
32+
var grandParent = resource(5L);
33+
var rootToChild = new ResourceEdge(root, child, TITLE);
34+
root.addOutgoingEdge(rootToChild);
35+
child.addIncomingEdge(rootToChild);
36+
var childToGrandChild = new ResourceEdge(child, grandChild, TITLE);
37+
child.addOutgoingEdge(childToGrandChild);
38+
grandChild.addIncomingEdge(childToGrandChild);
39+
var parentToRoot = new ResourceEdge(parent, root, TITLE);
40+
parent.addOutgoingEdge(parentToRoot);
41+
root.addIncomingEdge(parentToRoot);
42+
var grandParentToParent = new ResourceEdge(grandParent, parent, TITLE);
43+
grandParent.addOutgoingEdge(grandParentToParent);
44+
parent.addIncomingEdge(grandParentToParent);
2245

2346
// when
24-
var model = mapper.toModel(root, 0);
47+
var model = mapper.toModel(root, depth);
2548

2649
// then
27-
assertThat(model.getOutgoingEdges()).isEmpty();
50+
assertThat(model.getOutgoingEdges()).hasSize(expectedLevel1Size);
51+
assertThat(model.getIncomingEdges()).hasSize(expectedLevel1Size);
52+
if (expectedLevel1Size > 0) {
53+
assertThat(model.getOutgoingEdges().iterator().next().getTarget().getOutgoingEdges()).hasSize(expectedLevel2Size);
54+
assertThat(model.getIncomingEdges().iterator().next().getSource().getIncomingEdges()).hasSize(expectedLevel2Size);
55+
}
2856
}
2957

3058
@Test
31-
void toModel_depthOne_shouldMapOnlyFirstLevelOutgoingEdges() {
59+
void toModel_depthOne_outgoingAndIncomingDepthsAreIndependent() {
3260
// given
33-
var root = buildThreeNodeGraph();
61+
var root = resource(1L);
62+
var child = resource(2L);
63+
var grandChild = resource(3L);
64+
var parent = resource(4L);
65+
var grandParent = resource(5L);
66+
var rootToChild = new ResourceEdge(root, child, TITLE);
67+
root.addOutgoingEdge(rootToChild);
68+
child.addIncomingEdge(rootToChild);
69+
var childToGrandChild = new ResourceEdge(child, grandChild, TITLE);
70+
child.addOutgoingEdge(childToGrandChild);
71+
grandChild.addIncomingEdge(childToGrandChild);
72+
var parentToRoot = new ResourceEdge(parent, root, TITLE);
73+
parent.addOutgoingEdge(parentToRoot);
74+
root.addIncomingEdge(parentToRoot);
75+
var grandParentToParent = new ResourceEdge(grandParent, parent, TITLE);
76+
grandParent.addOutgoingEdge(grandParentToParent);
77+
parent.addIncomingEdge(grandParentToParent);
3478

3579
// when
3680
var model = mapper.toModel(root, 1);
3781

3882
// then
3983
assertThat(model.getOutgoingEdges()).hasSize(1);
40-
var child = model.getOutgoingEdges().iterator().next().getTarget();
41-
assertThat(child.getOutgoingEdges()).isEmpty();
42-
}
84+
assertThat(model.getIncomingEdges()).hasSize(1);
4385

44-
@Test
45-
void toModel_depthTwo_shouldMapTwoLevelsOfOutgoingEdges() {
46-
// given
47-
var root = buildThreeNodeGraph();
48-
49-
// when
50-
var model = mapper.toModel(root, 2);
86+
var mappedChild = model.getOutgoingEdges().iterator().next().getTarget();
87+
assertThat(mappedChild.getOutgoingEdges()).isEmpty();
88+
assertThat(mappedChild.getIncomingEdges()).hasSize(1);
5189

52-
// then
53-
assertThat(model.getOutgoingEdges()).hasSize(1);
54-
var child = model.getOutgoingEdges().iterator().next().getTarget();
55-
assertThat(child.getOutgoingEdges()).hasSize(1);
56-
var grandChild = child.getOutgoingEdges().iterator().next().getTarget();
57-
assertThat(grandChild.getOutgoingEdges()).isEmpty();
90+
var mappedParent = model.getIncomingEdges().iterator().next().getSource();
91+
assertThat(mappedParent.getIncomingEdges()).isEmpty();
92+
assertThat(mappedParent.getOutgoingEdges()).hasSize(1);
5893
}
5994

6095
@Test
6196
void toModel_depthGreaterThanMax_shouldThrow() {
6297
// given
63-
var root = buildThreeNodeGraph();
98+
var root = buildThreeNodeOutgoingGraph();
6499

65100
// when / then
66101
assertThatThrownBy(() -> mapper.toModel(root, 8))
67102
.isInstanceOf(IllegalArgumentException.class)
68-
.hasMessage("Requested outgoing edges depth is too high: 8. Maximum allowed is 7");
103+
.hasMessage("Requested edges depth is too high: 8. Maximum allowed is 7");
69104
}
70105

71-
private static Resource buildThreeNodeGraph() {
106+
private static Resource buildThreeNodeOutgoingGraph() {
72107
var root = resource(1L);
73108
var child = resource(2L);
74109
var grandChild = resource(3L);
75110

76-
root.addOutgoingEdge(new ResourceEdge(root, child, PredicateDictionary.TITLE));
77-
child.addOutgoingEdge(new ResourceEdge(child, grandChild, PredicateDictionary.TITLE));
111+
root.addOutgoingEdge(new ResourceEdge(root, child, TITLE));
112+
child.addOutgoingEdge(new ResourceEdge(child, grandChild, TITLE));
78113

79114
return root;
80115
}
@@ -85,3 +120,4 @@ private static Resource resource(Long id) {
85120
.addTypes(ResourceTypeDictionary.INSTANCE);
86121
}
87122
}
123+

0 commit comments

Comments
 (0)