diff --git a/src/main/java/no/ndla/taxonomy/domain/Node.java b/src/main/java/no/ndla/taxonomy/domain/Node.java index 413064f4..9b571e04 100644 --- a/src/main/java/no/ndla/taxonomy/domain/Node.java +++ b/src/main/java/no/ndla/taxonomy/domain/Node.java @@ -13,8 +13,10 @@ import java.time.Instant; import java.util.*; import java.util.stream.Collectors; +import no.ndla.taxonomy.config.Constants; import no.ndla.taxonomy.domain.exceptions.ChildNotFoundException; import no.ndla.taxonomy.domain.exceptions.DuplicateIdException; +import no.ndla.taxonomy.util.PrettyUrlUtil; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.Type; import org.hibernate.annotations.UpdateTimestamp; @@ -746,4 +748,20 @@ public List getTranslations() { public void setTranslations(List translations) { this.translations = translations; } + + public Set translatedPrettyNames() { + var pretties = this.translations.stream() + .map(JsonTranslation::getName) + .filter(Objects::nonNull) + .map(PrettyUrlUtil::prettyName) + .collect(Collectors.toSet()); + getPrettyName().ifPresent(pretties::add); + return pretties; + } + + public Optional getPrettyName() { + var defaultTranslation = this.getTranslatedName(Constants.DefaultLanguage); + var name = Optional.ofNullable(defaultTranslation); + return name.map(PrettyUrlUtil::prettyName); + } } diff --git a/src/main/java/no/ndla/taxonomy/rest/v1/dtos/searchapi/TaxonomyContextDTO.java b/src/main/java/no/ndla/taxonomy/rest/v1/dtos/searchapi/TaxonomyContextDTO.java index b3ae80e7..05cda6b7 100644 --- a/src/main/java/no/ndla/taxonomy/rest/v1/dtos/searchapi/TaxonomyContextDTO.java +++ b/src/main/java/no/ndla/taxonomy/rest/v1/dtos/searchapi/TaxonomyContextDTO.java @@ -33,6 +33,7 @@ "rank", "connectionId", "url", + "defaultUrl", "parents", "parentContextIds", "relevanceId", @@ -99,5 +100,8 @@ public record TaxonomyContextDTO( @JsonProperty @Schema(description = "Pretty-url of this particular context") String url, + @JsonProperty @Schema(description = "Pretty-url of this particular context in the default language") + String defaultUrl, + @JsonProperty @Schema(description = "List of all parents to this context. Empty if node is fetched as child") List parents) {} diff --git a/src/main/java/no/ndla/taxonomy/service/NodeService.java b/src/main/java/no/ndla/taxonomy/service/NodeService.java index 1fda592a..bc3b5185 100644 --- a/src/main/java/no/ndla/taxonomy/service/NodeService.java +++ b/src/main/java/no/ndla/taxonomy/service/NodeService.java @@ -358,6 +358,22 @@ public List nodesToContexts(List nodes, boolean filter } }) .toList(); + var url = PrettyUrlUtil.createPrettyUrl( + Optional.of(context.rootName()), + LanguageField.fromNode(node), + language, + context.contextId(), + node.getNodeType()) + .orElse(context.path()); + + var defaultUrl = PrettyUrlUtil.createPrettyUrl( + Optional.of(context.rootName()), + LanguageField.fromNode(node), + Constants.DefaultLanguage, + context.contextId(), + node.getNodeType()) + .orElse(context.path()); + return new TaxonomyContextDTO( node.getPublicId(), node.getPublicId(), @@ -378,13 +394,8 @@ public List nodesToContexts(List nodes, boolean filter context.contextId(), context.rank(), context.connectionId(), - PrettyUrlUtil.createPrettyUrl( - Optional.of(context.rootName()), - LanguageField.fromNode(node), - language, - context.contextId(), - node.getNodeType()) - .orElse(context.path()), + url, + defaultUrl, parents); }); }) diff --git a/src/main/java/no/ndla/taxonomy/service/dtos/NodeDTO.java b/src/main/java/no/ndla/taxonomy/service/dtos/NodeDTO.java index eba29969..daf69c55 100644 --- a/src/main/java/no/ndla/taxonomy/service/dtos/NodeDTO.java +++ b/src/main/java/no/ndla/taxonomy/service/dtos/NodeDTO.java @@ -91,6 +91,10 @@ public class NodeDTO { @Schema(description = "A pretty url based on name and context. Empty if no context.") private Optional url = Optional.empty(); + @JsonProperty + @Schema(description = "A pretty url based on the name and context in the default language.") + private Optional defaultUrl = Optional.empty(); + @JsonProperty @Schema(description = "A list of all contexts this node is part of") private List contexts = new ArrayList<>(); @@ -118,6 +122,14 @@ public class NodeDTO { @Schema(description = "The technical evaluation of the node.") private Optional technicalEvaluation = Optional.empty(); + @JsonProperty + @Schema(description = "Url safe names for the node.") + private Set urlName = new TreeSet<>(); + + @JsonProperty + @Schema(description = "Url safe name for the node in the default language.") + private String defaultUrlName; + public NodeDTO() {} public NodeDTO( @@ -154,6 +166,9 @@ public NodeDTO( this.supportedLanguages = this.translations.stream().map(t -> t.language).collect(Collectors.toCollection(TreeSet::new)); + this.urlName = entity.translatedPrettyNames(); + this.defaultUrlName = entity.getPrettyName().orElse(null); + this.language = languageCode; // TODO: Must handle programmes and subject better // this.language = supportedLanguages.contains(languageCode) // ? languageCode @@ -189,6 +204,7 @@ public NodeDTO( this.relevanceId = Optional.of(contextDto.relevanceId()); this.contextId = Optional.of(contextDto.contextId()); this.url = Optional.of(contextDto.url()); + this.defaultUrl = Optional.of(contextDto.defaultUrl()); this.context = Optional.of(contextDto); }); @@ -227,6 +243,22 @@ private TaxonomyContextDTO getTaxonomyContextDTO( .filter(Objects::nonNull) .toList(); var relevance = Relevance.unsafeGetRelevance(URI.create(ctx.relevanceId())); + var url = PrettyUrlUtil.createPrettyUrl( + Optional.of(ctx.rootName()), + LanguageField.fromNode(entity), + this.language, + ctx.contextId(), + entity.getNodeType()) + .orElse(ctx.path()); + + var defaultUrl = PrettyUrlUtil.createPrettyUrl( + Optional.of(ctx.rootName()), + LanguageField.fromNode(entity), + Constants.DefaultLanguage, + ctx.contextId(), + entity.getNodeType()) + .orElse(ctx.path()); + return new TaxonomyContextDTO( entity.getPublicId(), entity.getPublicId(), @@ -254,13 +286,8 @@ private TaxonomyContextDTO getTaxonomyContextDTO( ctx.contextId(), ctx.rank(), ctx.connectionId(), - PrettyUrlUtil.createPrettyUrl( - Optional.of(ctx.rootName()), - LanguageField.fromNode(entity), - this.language, - ctx.contextId(), - entity.getNodeType()) - .orElse(ctx.path()), + url, + defaultUrl, parents); } diff --git a/src/main/java/no/ndla/taxonomy/util/PrettyUrlUtil.java b/src/main/java/no/ndla/taxonomy/util/PrettyUrlUtil.java index 5ef4c801..25d58589 100644 --- a/src/main/java/no/ndla/taxonomy/util/PrettyUrlUtil.java +++ b/src/main/java/no/ndla/taxonomy/util/PrettyUrlUtil.java @@ -42,6 +42,12 @@ public static Optional createPrettyUrl( return Optional.of(builder.toString()); } + public static String prettyName(String name) { + var builder = new StringBuilder(); + buildUrlFragment(builder, cleanString(name)); + return builder.toString(); + } + private static String nodeTypeMapping(NodeType nodeType) { return switch (nodeType) { case SUBJECT -> "/f"; diff --git a/src/test/java/no/ndla/taxonomy/domain/Builder.java b/src/test/java/no/ndla/taxonomy/domain/Builder.java index 76e7074b..cc2f2bf8 100644 --- a/src/test/java/no/ndla/taxonomy/domain/Builder.java +++ b/src/test/java/no/ndla/taxonomy/domain/Builder.java @@ -128,7 +128,6 @@ public Node node(String key, NodeType nodeType, Consumer consumer) if (null != consumer) consumer.accept(node); entityManager.persist(node.node); - contextUpdaterService.updateContexts(node.node); return node.node; @@ -377,7 +376,6 @@ public NodeBuilder child(String key, NodeType nodeType, Consumer co public NodeBuilder child(Node child) { entityManager.persist(NodeConnection.create(node, child, Relevance.CORE)); - contextUpdaterService.updateContexts(child); return this; diff --git a/src/test/java/no/ndla/taxonomy/domain/NodeUrlNameTest.java b/src/test/java/no/ndla/taxonomy/domain/NodeUrlNameTest.java new file mode 100644 index 00000000..d989a868 --- /dev/null +++ b/src/test/java/no/ndla/taxonomy/domain/NodeUrlNameTest.java @@ -0,0 +1,25 @@ +/* + * Part of NDLA taxonomy-api + * Copyright (C) 2026 NDLA + * + * See LICENSE + */ + +package no.ndla.taxonomy.domain; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; +import org.junit.jupiter.api.Test; + +class NodeUrlNameTest { + + @Test + void translatedPrettyNamesSkipsNullTranslationNames() { + var node = new Node(NodeType.TOPIC); + node.setName("Default Name"); + node.addTranslation(new JsonTranslation(null, "nn")); + + assertEquals(Set.of("default-name"), node.translatedPrettyNames()); + } +} diff --git a/src/test/java/no/ndla/taxonomy/service/dtos/NodeDTOTest.java b/src/test/java/no/ndla/taxonomy/service/dtos/NodeDTOTest.java new file mode 100644 index 00000000..fb3bfedb --- /dev/null +++ b/src/test/java/no/ndla/taxonomy/service/dtos/NodeDTOTest.java @@ -0,0 +1,41 @@ +/* + * Part of NDLA taxonomy-api + * Copyright (C) 2026 NDLA + * + * See LICENSE + */ + +package no.ndla.taxonomy.service.dtos; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.test.util.ReflectionTestUtils.getField; + +import java.util.Optional; +import no.ndla.taxonomy.domain.Node; +import no.ndla.taxonomy.domain.NodeConnectionType; +import no.ndla.taxonomy.domain.NodeType; +import org.junit.jupiter.api.Test; + +class NodeDTOTest { + + @Test + void defaultUrlNameUsesDefaultLanguageTranslation() { + var node = new Node(NodeType.TOPIC); + node.setName("English base name"); + node.addTranslation("Bokmal default name", "nb"); + + var dto = new NodeDTO( + Optional.empty(), + Optional.empty(), + node, + NodeConnectionType.BRANCH, + "en", + Optional.empty(), + false, + false, + true, + false); + + assertEquals("bokmal-default-name", getField(dto, "defaultUrlName")); + } +}