From d2ca4d1b9ea153c57457e3222c0ff3cc430b8a93 Mon Sep 17 00:00:00 2001 From: PK Jacob Date: Tue, 17 Feb 2026 14:21:50 -0500 Subject: [PATCH] MODLD-913: Make the label of CONCEPT resource predictable --- NEWS.md | 3 +- .../generators/ConceptLabelGenerator.java | 44 ++++++++++++++++--- .../label/ConceptLabelGeneratorTest.java | 36 ++++++++++++--- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/NEWS.md b/NEWS.md index bd23213..5e3405e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,6 @@ ## v2.0.0 (IN PROGRESS) +- Add LIGHT_RESOURCE type [MODLD-980](https://folio-org.atlassian.net/browse/MODLD-980) +- Concept label generation now orders labels as FOCUS first, followed by sorted SUB_FOCUS entries [MODLD-913](https://folio-org.atlassian.net/browse/MODLD-913) ## v1.9.9 (13-02-2026) - Update Mapping for 300$b & $e (Physical description & Accompanying material) [MODLD-508](https://folio-org.atlassian.net/browse/MODLD-508) @@ -30,7 +32,6 @@ - Update the IRI of ID_LOCAL to http://library.link/identifier/LOCAL [MODLD-939](https://folio-org.atlassian.net/browse/MODLD-939) - Add resource types FAST, GND, LCCSH, LCDGT, LCGFT, LCMPT, LCNAF, LCSH, MESH, VIAF and WIKIID [MODLD-939](https://folio-org.atlassian.net/browse/MODLD-939) - Create HubLabelGenerator to generate labels for Hubs [MODLD-970](https://folio-org.atlassian.net/browse/MODLD-970) -- Add LIGHT_RESOURCE type [MODLD-980](https://folio-org.atlassian.net/browse/MODLD-980) ## v1.0.1 (03-12-2025) - Initial release diff --git a/src/main/java/org/folio/ld/dictionary/label/generators/ConceptLabelGenerator.java b/src/main/java/org/folio/ld/dictionary/label/generators/ConceptLabelGenerator.java index 3d5ab8f..92f1175 100644 --- a/src/main/java/org/folio/ld/dictionary/label/generators/ConceptLabelGenerator.java +++ b/src/main/java/org/folio/ld/dictionary/label/generators/ConceptLabelGenerator.java @@ -1,18 +1,26 @@ package org.folio.ld.dictionary.label.generators; -import static java.util.Comparator.comparing; +import static java.lang.String.CASE_INSENSITIVE_ORDER; import static org.folio.ld.dictionary.PredicateDictionary.FOCUS; import static org.folio.ld.dictionary.PredicateDictionary.SUB_FOCUS; import static org.folio.ld.dictionary.ResourceTypeDictionary.CONCEPT; +import static org.folio.ld.dictionary.ResourceTypeDictionary.FORM; +import static org.folio.ld.dictionary.ResourceTypeDictionary.PLACE; +import static org.folio.ld.dictionary.ResourceTypeDictionary.TEMPORAL; +import static org.folio.ld.dictionary.ResourceTypeDictionary.TOPIC; import java.util.Comparator; +import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; +import org.folio.ld.dictionary.ResourceTypeDictionary; import org.folio.ld.dictionary.label.LabelGenerator; import org.folio.ld.dictionary.model.Resource; import org.folio.ld.dictionary.model.ResourceEdge; public class ConceptLabelGenerator implements LabelGenerator { private static final String SEPARATOR_CONCEPT = " -- "; + private static final Comparator CONCEPT_EDGES_COMPARATOR = new ConceptEdgesComparator(); @Override public boolean supports(Resource resource) { @@ -24,13 +32,37 @@ public String getLabel(Resource resource) { return resource.getOutgoingEdges() .stream() .filter(re -> re.getPredicate() == FOCUS || re.getPredicate() == SUB_FOCUS) - .sorted((o1, o2) -> { - Comparator comparing = comparing(o -> o.getPredicate().name()); - return comparing.thenComparing(re -> re.getTarget().getLabel()) - .compare(o1, o2); - }) + .sorted(CONCEPT_EDGES_COMPARATOR) .map(ResourceEdge::getTarget) .map(Resource::getLabel) .collect(Collectors.joining(SEPARATOR_CONCEPT)); } + + private static final class ConceptEdgesComparator implements Comparator { + private static final Map TYPE_ORDER_INDEX = Map.of( + TOPIC, 0, + PLACE, 1, + TEMPORAL, 2, + FORM, 3 + ); + + @Override + public int compare(ResourceEdge edge1, ResourceEdge edge2) { + return Comparator.comparing((ResourceEdge edge) -> edge.getPredicate().name()) + .thenComparingInt(this::getSubFocusTypeOrder) + .thenComparing(e -> e.getTarget().getLabel(), Comparator.nullsFirst(CASE_INSENSITIVE_ORDER)) + .compare(edge1, edge2); + } + + private int getSubFocusTypeOrder(ResourceEdge edge) { + if (edge.getPredicate() != SUB_FOCUS) { + return -1; + } + return edge.getTarget().getTypes().stream() + .map(TYPE_ORDER_INDEX::get) + .filter(Objects::nonNull) + .min(Integer::compareTo) + .orElse(TYPE_ORDER_INDEX.size()); + } + } } diff --git a/src/test/java/org/folio/ld/dictionary/label/ConceptLabelGeneratorTest.java b/src/test/java/org/folio/ld/dictionary/label/ConceptLabelGeneratorTest.java index 8d0d5c1..9326e21 100644 --- a/src/test/java/org/folio/ld/dictionary/label/ConceptLabelGeneratorTest.java +++ b/src/test/java/org/folio/ld/dictionary/label/ConceptLabelGeneratorTest.java @@ -4,7 +4,11 @@ import static org.folio.ld.dictionary.PredicateDictionary.FOCUS; import static org.folio.ld.dictionary.PredicateDictionary.SUB_FOCUS; import static org.folio.ld.dictionary.ResourceTypeDictionary.CONCEPT; +import static org.folio.ld.dictionary.ResourceTypeDictionary.FORM; import static org.folio.ld.dictionary.ResourceTypeDictionary.INSTANCE; +import static org.folio.ld.dictionary.ResourceTypeDictionary.PLACE; +import static org.folio.ld.dictionary.ResourceTypeDictionary.TEMPORAL; +import static org.folio.ld.dictionary.ResourceTypeDictionary.TOPIC; import org.folio.ld.dictionary.model.Resource; import org.folio.ld.dictionary.model.ResourceEdge; @@ -112,7 +116,7 @@ void generateLabel_shouldReturnEmptyStringForConceptWithNoFocusOrSubFocus() { } @Test - void generateLabel_shouldGenerateLabelForConceptWithMultipleFocusAndSubFocus() { + void generateLabel_shouldGenerateLabelForConceptWithFocusAndMultipleSortedSubFocus() { // given var focusResource1 = new Resource() .setId(2L) @@ -124,25 +128,45 @@ void generateLabel_shouldGenerateLabelForConceptWithMultipleFocusAndSubFocus() { var subFocusResource1 = new Resource() .setId(4L) - .setLabel("SubFocus 1"); + .setLabel("zebra") + .addType(FORM); var subFocusResource2 = new Resource() .setId(5L) - .setLabel("SubFocus 2"); + .setLabel("Beta") + .addType(TEMPORAL); + + var subFocusResource3 = new Resource() + .setId(6L) + .setLabel("alpha") + .addType(TEMPORAL); + + var subFocusResource4 = new Resource() + .setId(7L) + .setLabel("topic-a") + .addType(TOPIC); + + var subFocusResource5 = new Resource() + .setId(8L) + .setLabel("place-a") + .addType(PLACE); var concept = new Resource() .setId(1L) .addType(CONCEPT); - concept.addOutgoingEdge(new ResourceEdge(concept, focusResource1, FOCUS)); + concept.addOutgoingEdge(new ResourceEdge(concept, subFocusResource5, SUB_FOCUS)); + concept.addOutgoingEdge(new ResourceEdge(concept, subFocusResource2, SUB_FOCUS)); concept.addOutgoingEdge(new ResourceEdge(concept, focusResource2, FOCUS)); + concept.addOutgoingEdge(new ResourceEdge(concept, subFocusResource3, SUB_FOCUS)); + concept.addOutgoingEdge(new ResourceEdge(concept, focusResource1, FOCUS)); concept.addOutgoingEdge(new ResourceEdge(concept, subFocusResource1, SUB_FOCUS)); - concept.addOutgoingEdge(new ResourceEdge(concept, subFocusResource2, SUB_FOCUS)); + concept.addOutgoingEdge(new ResourceEdge(concept, subFocusResource4, SUB_FOCUS)); // when var label = generator.getLabel(concept); // then - assertThat(label).isEqualTo("Focus 1 -- Focus 2 -- SubFocus 1 -- SubFocus 2"); + assertThat(label).isEqualTo("Focus 1 -- Focus 2 -- topic-a -- place-a -- alpha -- Beta -- zebra"); } }