From 7e70f5688a71bc9d0782adda8c0bd6a22db08c50 Mon Sep 17 00:00:00 2001 From: Stig Norland Date: Fri, 17 Jan 2025 13:37:35 +0100 Subject: [PATCH] hotfix/NP-48375-accessLevel (#634) #634 NP-48375 --- ...nva.search.service.java-conventions.gradle | 8 +- .../nva/search/common/OpenSearchClient.java | 3 - .../nva/search/common/ParameterValidator.java | 4 +- .../unit/nva/search/common/QueryFilter.java | 4 +- .../unit/nva/search/common/SearchQuery.java | 3 +- .../search/resource/ResourceAccessFilter.java | 81 +++-- .../search/resource/SimplifiedMutator.java | 293 +++++++----------- .../search/resource/response/Affiliation.java | 12 +- .../search/resource/response/Identity.java | 15 +- .../search/resource/response/NodeUtils.java | 12 + .../search/resource/response/Publisher.java | 5 - .../resource/response/PublishingDetails.java | 7 +- .../response/ResourceSearchResponse.java | 46 ++- .../resource/response/ScientificRating.java | 17 + .../nva/search/resource/response/Series.java | 5 - .../search/resource/ResourceClientTest.java | 69 +++-- .../test/resources/resource_datasource.json | 49 ++- .../nva/search/SearchResourceAuthHandler.java | 16 +- .../search/SearchResourceAuthHandlerTest.java | 1 + 19 files changed, 335 insertions(+), 315 deletions(-) create mode 100644 search-commons/src/main/java/no/unit/nva/search/resource/response/NodeUtils.java delete mode 100644 search-commons/src/main/java/no/unit/nva/search/resource/response/Publisher.java create mode 100644 search-commons/src/main/java/no/unit/nva/search/resource/response/ScientificRating.java delete mode 100644 search-commons/src/main/java/no/unit/nva/search/resource/response/Series.java diff --git a/buildSrc/src/main/groovy/nva.search.service.java-conventions.gradle b/buildSrc/src/main/groovy/nva.search.service.java-conventions.gradle index 7769d92c6..77e1504f3 100644 --- a/buildSrc/src/main/groovy/nva.search.service.java-conventions.gradle +++ b/buildSrc/src/main/groovy/nva.search.service.java-conventions.gradle @@ -81,7 +81,7 @@ spotless { format 'misc', { target '.gitignore', '.gitattributes', '.editorconfig', '**/*.gradle' - indentWithSpaces(4) + leadingTabsToSpaces(4) trimTrailingWhitespace() endWithNewline() } @@ -96,12 +96,16 @@ tasks.named('test').configure { dependsOn 'spotlessApply' } - pmd { toolVersion = '7.5.0' ruleSetConfig = rootProject.resources.text.fromFile('config/pmd/ruleset.xml') ruleSets = [] ignoreFailures = false + pmdMain { + excludes = [ + '**/generated/.*' + ] + } } checkstyle { diff --git a/search-commons/src/main/java/no/unit/nva/search/common/OpenSearchClient.java b/search-commons/src/main/java/no/unit/nva/search/common/OpenSearchClient.java index a4817e56f..f0078a1cf 100644 --- a/search-commons/src/main/java/no/unit/nva/search/common/OpenSearchClient.java +++ b/search-commons/src/main/java/no/unit/nva/search/common/OpenSearchClient.java @@ -57,9 +57,6 @@ public OpenSearchClient(HttpClient httpClient, CachedJwtProvider jwtProvider) { } public R doSearch(Q query) { - if (query.filters().hasContent()) { - logger.info(query.filters().toString()); - } queryBuilderStart = query.getStartTime(); queryParameters = query.parameters().asMap().entrySet().stream() diff --git a/search-commons/src/main/java/no/unit/nva/search/common/ParameterValidator.java b/search-commons/src/main/java/no/unit/nva/search/common/ParameterValidator.java index 80c306aed..b6bd21362 100644 --- a/search-commons/src/main/java/no/unit/nva/search/common/ParameterValidator.java +++ b/search-commons/src/main/java/no/unit/nva/search/common/ParameterValidator.java @@ -180,7 +180,9 @@ public ParameterValidator fromRequestInfo(RequestInfo requestInfo) { var contentType = extractContentTypeFromRequestInfo(requestInfo); query.setMediaType(isNull(contentType) ? null : contentType.getMimeType()); var uri = URI.create(HTTPS + requestInfo.getDomainName() + requestInfo.getPath()); - query.setAccessRights(requestInfo.getAccessRights()); + if (requestInfo.getHeaders().containsKey("Authorization")) { + query.setAccessRights(requestInfo.getAccessRights()); + } query.setNvaSearchApiUri(uri); return fromMultiValueParameters(requestInfo.getMultiValueQueryStringParameters()); } diff --git a/search-commons/src/main/java/no/unit/nva/search/common/QueryFilter.java b/search-commons/src/main/java/no/unit/nva/search/common/QueryFilter.java index 73d09b98e..99bdae8f4 100644 --- a/search-commons/src/main/java/no/unit/nva/search/common/QueryFilter.java +++ b/search-commons/src/main/java/no/unit/nva/search/common/QueryFilter.java @@ -39,8 +39,8 @@ public void set(QueryBuilder... filters) { Arrays.stream(filters).forEach(this::add); } - public boolean hasContent() { - return !filters.isEmpty(); + public int size() { + return filters.size(); } public QueryFilter add(QueryBuilder builder) { diff --git a/search-commons/src/main/java/no/unit/nva/search/common/SearchQuery.java b/search-commons/src/main/java/no/unit/nva/search/common/SearchQuery.java index 32483d143..3d6afc33a 100644 --- a/search-commons/src/main/java/no/unit/nva/search/common/SearchQuery.java +++ b/search-commons/src/main/java/no/unit/nva/search/common/SearchQuery.java @@ -331,8 +331,7 @@ private void handleSearchAfter(SearchSourceBuilder builder) { private void handleSorting(SearchSourceBuilder builder) { if (hasSortBy(RELEVANCE_KEY_NAME)) { - // Not very well documented. - // This allows sorting on relevance together with other fields. + // This allows sorting on relevance together with other fields. (Not very well documented) builder.trackScores(true); } builderStreamFieldSort().forEach(builder::sort); diff --git a/search-commons/src/main/java/no/unit/nva/search/resource/ResourceAccessFilter.java b/search-commons/src/main/java/no/unit/nva/search/resource/ResourceAccessFilter.java index ec8377e20..a80b31f43 100644 --- a/search-commons/src/main/java/no/unit/nva/search/resource/ResourceAccessFilter.java +++ b/search-commons/src/main/java/no/unit/nva/search/resource/ResourceAccessFilter.java @@ -1,27 +1,32 @@ package no.unit.nva.search.resource; +import static no.unit.nva.constants.Words.AUTHORIZATION; import static no.unit.nva.constants.Words.CURATING_INSTITUTIONS; import static no.unit.nva.constants.Words.DOT; import static no.unit.nva.constants.Words.KEYWORD; import static no.unit.nva.constants.Words.STATUS; +import static no.unit.nva.search.common.enums.PublicationStatus.DELETED; import static no.unit.nva.search.common.enums.PublicationStatus.PUBLISHED; -import static no.unit.nva.search.common.enums.PublicationStatus.PUBLISHED_METADATA; import static no.unit.nva.search.common.enums.PublicationStatus.UNPUBLISHED; import static no.unit.nva.search.resource.Constants.CONTRIBUTOR_ORG_KEYWORD; import static no.unit.nva.search.resource.Constants.STATUS_KEYWORD; import static no.unit.nva.search.resource.ResourceParameter.STATISTICS; import static nva.commons.apigateway.AccessRight.MANAGE_CUSTOMERS; import static nva.commons.apigateway.AccessRight.MANAGE_RESOURCES_ALL; +import static nva.commons.apigateway.AccessRight.MANAGE_RESOURCES_STANDARD; import java.net.URI; import java.util.Arrays; +import java.util.stream.Stream; import no.unit.nva.search.common.enums.PublicationStatus; import no.unit.nva.search.common.records.FilterBuilder; import nva.commons.apigateway.RequestInfo; import nva.commons.apigateway.exceptions.UnauthorizedException; +import org.opensearch.index.query.DisMaxQueryBuilder; import org.opensearch.index.query.QueryBuilder; -import org.opensearch.index.query.QueryBuilders; import org.opensearch.index.query.TermsQueryBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * ResourceAccessFilter is a class that filters tickets based on access rights. @@ -41,6 +46,7 @@ public class ResourceAccessFilter implements FilterBuilder private static final String EDITOR_FILTER = "EditorFilter"; private static final String CURATOR_FILTER = "CuratorFilter"; + protected static final Logger logger = LoggerFactory.getLogger(ResourceAccessFilter.class); private final ResourceSearchQuery searchQuery; public ResourceAccessFilter(ResourceSearchQuery query) { @@ -62,10 +68,11 @@ public ResourceSearchQuery apply() { @Override public ResourceSearchQuery fromRequestInfo(RequestInfo requestInfo) throws UnauthorizedException { - - return customerCurationInstitutions(requestInfo) - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) - .apply(); + if (isAuthorized(requestInfo)) { + return customerCurationInstitutions(requestInfo).apply(); + } else { + return requiredStatus(PUBLISHED).apply(); + } } /** @@ -82,12 +89,8 @@ public ResourceSearchQuery fromRequestInfo(RequestInfo requestInfo) throws Unaut */ public ResourceAccessFilter requiredStatus(PublicationStatus... publicationStatus) { final var values = - Arrays.stream(publicationStatus) - .filter(this::isStatusAllowed) - .map(PublicationStatus::toString) - .toArray(String[]::new); - final var filter = new TermsQueryBuilder(STATUS_KEYWORD, values).queryName(STATUS); - this.searchQuery.filters().add(filter); + Arrays.stream(publicationStatus).map(PublicationStatus::toString).toArray(String[]::new); + this.searchQuery.filters().add(new TermsQueryBuilder(STATUS_KEYWORD, values).queryName(STATUS)); return this; } @@ -101,31 +104,50 @@ public ResourceAccessFilter requiredStatus(PublicationStatus... publicationStatu */ public ResourceAccessFilter customerCurationInstitutions(RequestInfo requestInfo) throws UnauthorizedException { - if (isCurator() && isStatisticsQuery()) { + if (isAppAdmin() && isStatisticsQuery()) { return this; } - final var filter = - QueryBuilders.boolQuery().minimumShouldMatch(1).queryName(EDITOR_CURATOR_FILTER); + + final var statuses = + Stream.of(PUBLISHED, DELETED, UNPUBLISHED) + .filter(this::isStatusAllowed) + .toArray(PublicationStatus[]::new); + + requiredStatus(statuses); + var curationInstitutionId = getCurationInstitutionId(requestInfo).toString(); if (isCurator()) { - filter.should(getCuratingInstitutionAccessFilter(curationInstitutionId)); - filter.should(getContributingOrganisationAccessFilter(curationInstitutionId)); + this.searchQuery.filters().add(buildMatchBoth(curationInstitutionId)); } else if (isEditor()) { - filter.should(getContributingOrganisationAccessFilter(curationInstitutionId)); + this.searchQuery.filters().add(filterByContributingOrg(curationInstitutionId)); } - if (!filter.hasClauses()) { + if (statusOrOrgfilterIsMissing() && !isAppAdmin()) { throw new UnauthorizedException(); } - this.searchQuery.filters().add(filter); return this; } - private QueryBuilder getContributingOrganisationAccessFilter(String institutionId) { - return QueryBuilders.termQuery(CONTRIBUTOR_ORG_KEYWORD, institutionId).queryName(EDITOR_FILTER); + private boolean statusOrOrgfilterIsMissing() { + return this.searchQuery.filters().size() < 2; + } + + private QueryBuilder filterByContributingOrg(String institutionId) { + return new TermsQueryBuilder(CONTRIBUTOR_ORG_KEYWORD, institutionId).queryName(EDITOR_FILTER); } - private QueryBuilder getCuratingInstitutionAccessFilter(String institutionId) { - return QueryBuilders.termQuery(CURATING_INST_KEYWORD, institutionId).queryName(CURATOR_FILTER); + private QueryBuilder filterByCuratingOrg(String institutionId) { + return new TermsQueryBuilder(CURATING_INST_KEYWORD, institutionId).queryName(CURATOR_FILTER); + } + + private DisMaxQueryBuilder buildMatchBoth(String institutionId) { + return new DisMaxQueryBuilder() + .add(filterByContributingOrg(institutionId)) + .add(filterByCuratingOrg(institutionId)) + .queryName(EDITOR_CURATOR_FILTER); + } + + private boolean isAuthorized(RequestInfo requestInfo) { + return requestInfo.getHeaders().containsKey(AUTHORIZATION); } /** @@ -135,17 +157,24 @@ private QueryBuilder getCuratingInstitutionAccessFilter(String institutionId) { * @return true if allowed */ private boolean isStatusAllowed(PublicationStatus publicationStatus) { - return isEditor() || publicationStatus != UNPUBLISHED; + return isAppAdmin() + || (isEditor() && publicationStatus == DELETED) + || publicationStatus == PUBLISHED + || publicationStatus == UNPUBLISHED; } private boolean isStatisticsQuery() { return searchQuery.parameters().isPresent(STATISTICS); } - private boolean isCurator() { + private boolean isAppAdmin() { return searchQuery.hasAccessRights(MANAGE_CUSTOMERS); } + private boolean isCurator() { + return searchQuery.hasAccessRights(MANAGE_RESOURCES_STANDARD); + } + private boolean isEditor() { return searchQuery.hasAccessRights(MANAGE_RESOURCES_ALL); } diff --git a/search-commons/src/main/java/no/unit/nva/search/resource/SimplifiedMutator.java b/search-commons/src/main/java/no/unit/nva/search/resource/SimplifiedMutator.java index c1037d667..3202043c2 100644 --- a/search-commons/src/main/java/no/unit/nva/search/resource/SimplifiedMutator.java +++ b/search-commons/src/main/java/no/unit/nva/search/resource/SimplifiedMutator.java @@ -26,7 +26,6 @@ import static no.unit.nva.constants.Words.PUBLISHER; import static no.unit.nva.constants.Words.REFERENCE; import static no.unit.nva.constants.Words.ROLE; -import static no.unit.nva.constants.Words.SCIENTIFIC_VALUE; import static no.unit.nva.constants.Words.SERIES; import static no.unit.nva.constants.Words.STATUS; import static no.unit.nva.constants.Words.TYPE; @@ -41,31 +40,14 @@ import static no.unit.nva.search.resource.Constants.MANIFESTATIONS; import static no.unit.nva.search.resource.Constants.SCOPUS_IDENTIFIER; import static no.unit.nva.search.resource.Constants.SEQUENCE; - +import static no.unit.nva.search.resource.response.ResourceSearchResponse.responseBuilder; import static nva.commons.core.attempt.Try.attempt; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; - -import no.unit.nva.search.common.records.JsonNodeMutator; -import no.unit.nva.search.resource.response.Affiliation; -import no.unit.nva.search.resource.response.Contributor; -import no.unit.nva.search.resource.response.Identity; -import no.unit.nva.search.resource.response.OtherIdentifiers; -import no.unit.nva.search.resource.response.PublicationDate; -import no.unit.nva.search.resource.response.Publisher; -import no.unit.nva.search.resource.response.PublishingDetails; -import no.unit.nva.search.resource.response.RecordMetadata; -import no.unit.nva.search.resource.response.ResourceSearchResponse; -import no.unit.nva.search.resource.response.ResourceSearchResponse.Builder; -import no.unit.nva.search.resource.response.Series; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - import java.io.IOException; -import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -75,6 +57,17 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import no.unit.nva.search.common.records.JsonNodeMutator; +import no.unit.nva.search.resource.response.Affiliation; +import no.unit.nva.search.resource.response.Contributor; +import no.unit.nva.search.resource.response.Identity; +import no.unit.nva.search.resource.response.NodeUtils; +import no.unit.nva.search.resource.response.OtherIdentifiers; +import no.unit.nva.search.resource.response.PublicationDate; +import no.unit.nva.search.resource.response.PublishingDetails; +import no.unit.nva.search.resource.response.RecordMetadata; +import no.unit.nva.search.resource.response.ResourceSearchResponse; +import no.unit.nva.search.resource.response.ScientificRating; public class SimplifiedMutator implements JsonNodeMutator { @@ -83,7 +76,11 @@ public class SimplifiedMutator implements JsonNodeMutator { private final ObjectMapper objectMapper = dtoObjectMapper.copy(); public SimplifiedMutator() { - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper + .configOverride(Map.class) + .setInclude( + JsonInclude.Value.construct( + JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL)); } public static String path(String... path) { @@ -124,88 +121,51 @@ public static List getIncludedFields() { path(ADDITIONAL_IDENTIFIERS, VALUE)); } - @NotNull - private static PublicationDate mutatePublicationDate(JsonNode source) { + @Override + public JsonNode transform(JsonNode source) { + return (JsonNode) attempt(() -> objectMapper.valueToTree(transformToDto(source))).orElseThrow(); + } + + private PublicationDate fromNodePublicationDate(JsonNode source) { return new PublicationDate( source.path(ENTITY_DESCRIPTION).path(PUBLICATION_DATE).path(YEAR).textValue(), source.path(ENTITY_DESCRIPTION).path(PUBLICATION_DATE).path(MONTH).textValue(), source.path(ENTITY_DESCRIPTION).path(PUBLICATION_DATE).path(DAY).textValue()); } - @Override - public JsonNode transform(JsonNode source) { - return (JsonNode) attempt(() -> objectMapper.valueToTree(transformToDto(source))).orElseThrow(); - } - - private ResourceSearchResponse transformToDto(JsonNode source) throws IOException { - return new Builder() - .withId(uriFromText(source.path(ID).textValue())) - .withIdentifier(source.path(IDENTIFIER).textValue()) - .withType( - source - .path(ENTITY_DESCRIPTION) - .path(REFERENCE) - .path(PUBLICATION_INSTANCE) - .path(TYPE) - .textValue()) - .withMainTitle(source.path(ENTITY_DESCRIPTION).path(MAIN_TITLE).textValue()) - .withMainLanguageAbstract(source.path(ENTITY_DESCRIPTION).path(ABSTRACT).textValue()) - .withDescription(source.path(ENTITY_DESCRIPTION).path(Constants.DESCRIPTION).textValue()) - .withAlternativeTitles(mutateAlternativeTitles(source)) - .withPublicationDate(mutatePublicationDate(source)) - .withContributorsPreview(mutateContributorsPreview(source)) - .withContributorsCount(source.path(ENTITY_DESCRIPTION).path(CONTRIBUTORS_COUNT).asInt()) - .withPublishingDetails(mutatePublishingDetails(source)) - .withOtherIdentifiers(mutateOtherIdentifiers(source)) - .withRecordMetadata(mutateRecordMetadata(source)) + private ResourceSearchResponse transformToDto(JsonNode source) { + return responseBuilder() + .withId(NodeUtils.toUri(source.path(ID))) + .withIdentifier(source.path(IDENTIFIER)) + .withMainTitle(source.path(ENTITY_DESCRIPTION).path(MAIN_TITLE)) + .withMainLanguageAbstract(source.path(ENTITY_DESCRIPTION).path(ABSTRACT)) + .withDescription(source.path(ENTITY_DESCRIPTION).path(Constants.DESCRIPTION)) + .withContributorsCount(source.path(ENTITY_DESCRIPTION).path(CONTRIBUTORS_COUNT)) + .withType(source) + .withAlternativeTitles(fromNodeAlternativeTitles(source)) + .withPublicationDate(fromNodePublicationDate(source)) + .withContributorsPreview(fromNodeContributorPreviews(source)) + .withPublishingDetails(fromNodePublishingDetails(source)) + .withOtherIdentifiers(fromNodeOtherIdentifiers(source)) + .withRecordMetadata(fromNodeRecordMetadata(source)) .build(); } - @Nullable - private Map mutateAlternativeTitles(JsonNode source) { - return source.path(ENTITY_DESCRIPTION).has(ALTERNATIVE_TITLES) - ? jsonNodeMapToMap(source.path(ENTITY_DESCRIPTION).path(ALTERNATIVE_TITLES)) - : null; + private Map fromNodeAlternativeTitles(JsonNode source) { + var path = source.path(ENTITY_DESCRIPTION).path(ALTERNATIVE_TITLES); + return path.isMissingNode() ? Collections.emptyMap() : jsonNodeMapToMap(path); } private Map jsonNodeMapToMap(JsonNode source) { - return objectMapper.convertValue(source, Map.class); + return objectMapper.convertValue(source, new TypeReference<>() {}); } - private OtherIdentifiers mutateOtherIdentifiers(JsonNode source) throws IOException { - var issns = getIssns(source); - - var isbnsInSourceNode = - source.path(ENTITY_DESCRIPTION).path(REFERENCE).path(PUBLICATION_CONTEXT).path(ISBN_LIST); - - List isbnsInManifestations = new ArrayList<>(); - var manifestations = - source - .path(ENTITY_DESCRIPTION) - .path(REFERENCE) - .path(PUBLICATION_INSTANCE) - .path(MANIFESTATIONS); - - if (!manifestations.isMissingNode() && manifestations.isArray()) { - manifestations - .iterator() - .forEachRemaining( - manifest -> { - if (!manifest.path(ISBN_LIST).isMissingNode()) { - manifest - .get(ISBN_LIST) - .elements() - .forEachRemaining(isbn -> isbnsInManifestations.add(isbn.textValue())); - } - }); - } - - List isbnsInSource = - isbnsInSourceNode.isMissingNode() - ? Collections.emptyList() - : objectMapper.readerForListOf(String.class).readValue(isbnsInSourceNode); - - var isbns = Stream.concat(isbnsInSource.stream(), isbnsInManifestations.stream()).toList(); + private OtherIdentifiers fromNodeOtherIdentifiers(JsonNode source) { + var isbnsInManifestations = isbnsInManifestations(source); + var isbnsInPublicationContext = extractIsbnsInPublicationContext(source); + var isbns = + Stream.concat(isbnsInPublicationContext.stream(), isbnsInManifestations.stream()).toList(); + var issns = fromNodeIssns(source); var handleIdentifiers = new ArrayList(); var cristinIdentifiers = new ArrayList(); @@ -231,101 +191,73 @@ private OtherIdentifiers mutateOtherIdentifiers(JsonNode source) throws IOExcept new HashSet<>(isbns)); } - private Set getIssns(JsonNode source) { - return Stream.of( - source - .path(ENTITY_DESCRIPTION) - .path(REFERENCE) - .path(PUBLICATION_CONTEXT) - .path(ONLINE_ISSN) - .textValue(), - source - .path(ENTITY_DESCRIPTION) - .path(REFERENCE) - .path(PUBLICATION_CONTEXT) - .path(PRINT_ISSN) - .textValue(), - source - .path(ENTITY_DESCRIPTION) - .path(REFERENCE) - .path(PUBLICATION_CONTEXT) - .path(SERIES) - .path(ONLINE_ISSN) - .textValue(), - source - .path(ENTITY_DESCRIPTION) - .path(REFERENCE) - .path(PUBLICATION_CONTEXT) - .path(SERIES) - .path(PRINT_ISSN) - .textValue()) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - } - - private RecordMetadata mutateRecordMetadata(JsonNode source) { - return new RecordMetadata( - source.path(STATUS).textValue(), source.path(CREATED_DATE).textValue(), - source.path(MODIFIED_DATE).textValue(), source.path(PUBLISHED_DATE).textValue()); - } - - private PublishingDetails mutatePublishingDetails(JsonNode source) { - return new PublishingDetails( - uriFromText( - source - .path(ENTITY_DESCRIPTION) - .path(REFERENCE) - .path(PUBLICATION_CONTEXT) - .path(ID) - .textValue()), - mutatePublicationContextType(source), - mutateSeries(source), + private List isbnsInManifestations(JsonNode source) { + List isbnsInManifestations = new ArrayList<>(); + var manifestations = source .path(ENTITY_DESCRIPTION) .path(REFERENCE) - .path(PUBLICATION_CONTEXT) - .path(NAME) - .textValue(), - uriFromText(source.path(ENTITY_DESCRIPTION).path(REFERENCE).path(DOI).textValue()), - mutatePublisher(source)); + .path(PUBLICATION_INSTANCE) + .path(MANIFESTATIONS); + + if (!manifestations.isMissingNode() && manifestations.isArray()) { + manifestations + .iterator() + .forEachRemaining( + manifest -> { + if (!manifest.path(ISBN_LIST).isMissingNode()) { + isbnsInManifestations.addAll(nodeAsListOf(manifest.path(ISBN_LIST))); + } + }); + } + return isbnsInManifestations; } - private String mutatePublicationContextType(JsonNode source) { - return source - .path(ENTITY_DESCRIPTION) - .path(REFERENCE) - .path(PUBLICATION_CONTEXT) - .path(TYPE) - .textValue(); + private List extractIsbnsInPublicationContext(JsonNode source) { + var isbnsInSourceNode = + source.path(ENTITY_DESCRIPTION).path(REFERENCE).path(PUBLICATION_CONTEXT).path(ISBN_LIST); + return isbnsInSourceNode.isMissingNode() + ? Collections.emptyList() + : nodeAsListOf(isbnsInSourceNode); } - private Series mutateSeries(JsonNode source) { - var series = - source.path(ENTITY_DESCRIPTION).path(REFERENCE).path(PUBLICATION_CONTEXT).path(SERIES); + private Set fromNodeIssns(JsonNode source) { + var context = source.path(ENTITY_DESCRIPTION).path(REFERENCE).path(PUBLICATION_CONTEXT); + return Stream.of( + context.path(ONLINE_ISSN).textValue(), + context.path(PRINT_ISSN).textValue(), + context.path(SERIES).path(ONLINE_ISSN).textValue(), + context.path(SERIES).path(PRINT_ISSN).textValue()) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } - if (series.isMissingNode()) { - return null; - } - return new Series( - uriFromText(series.path(ID).textValue()), - series.path(NAME).textValue(), - series.path(SCIENTIFIC_VALUE).textValue()); + private RecordMetadata fromNodeRecordMetadata(JsonNode source) { + return new RecordMetadata( + source.path(STATUS).textValue(), + source.path(CREATED_DATE).textValue(), + source.path(MODIFIED_DATE).textValue(), + source.path(PUBLISHED_DATE).textValue()); } - private Publisher mutatePublisher(JsonNode source) { - var publisher = - source.path(ENTITY_DESCRIPTION).path(REFERENCE).path(PUBLICATION_CONTEXT).path(PUBLISHER); + private PublishingDetails fromNodePublishingDetails(JsonNode source) { + var publicationContext = + source.path(ENTITY_DESCRIPTION).path(REFERENCE).path(PUBLICATION_CONTEXT); - if (publisher.isMissingNode()) { - return null; - } - return new Publisher( - uriFromText(publisher.path(ID).textValue()), - publisher.path(NAME).textValue(), - publisher.path(SCIENTIFIC_VALUE).textValue()); + return new PublishingDetails( + NodeUtils.toUri(publicationContext.path(ID)), + publicationContext.path(TYPE).textValue(), + publicationContext.path(NAME).textValue(), + NodeUtils.toUri(source.path(ENTITY_DESCRIPTION).path(REFERENCE).path(DOI)), + fromNodeRating(publicationContext.path(SERIES)), + fromNodeRating(publicationContext.path(PUBLISHER))); } - private List mutateContributorsPreview(JsonNode source) { + private ScientificRating fromNodeRating(JsonNode node) { + return node.isMissingNode() ? null : new ScientificRating(node); + } + + private List fromNodeContributorPreviews(JsonNode source) { var contributors = new ArrayList(); source .path(ENTITY_DESCRIPTION) @@ -338,28 +270,25 @@ private List mutateContributorsPreview(JsonNode source) { if (!affiliationNode.isMissingNode()) { affiliationNode .iterator() - .forEachRemaining( - aff -> - affiliations.add( - new Affiliation( - aff.path(ID).textValue(), aff.path(TYPE).textValue()))); + .forEachRemaining(node -> affiliations.add(new Affiliation(node))); } contributors.add( new Contributor( affiliations, contributorNode.path(CORRESPONDING_AUTHOR).asBoolean(), - new Identity( - uriFromText(contributorNode.path(IDENTITY).path(ID).textValue()), - contributorNode.path(IDENTITY).path(NAME).textValue(), - uriFromText(contributorNode.path(IDENTITY).path(ORC_ID).textValue())), + new Identity(contributorNode.path(IDENTITY)), contributorNode.path(ROLE).path(TYPE).textValue(), contributorNode.path(SEQUENCE).asInt())); }); return contributors; } - private URI uriFromText(String text) { - return Objects.isNull(text) ? null : URI.create(text); + private List nodeAsListOf(JsonNode jsonNode) { + try { + return objectMapper.readerForListOf(String.class).readValue(jsonNode); + } catch (IOException e) { + return Collections.emptyList(); + } } } diff --git a/search-commons/src/main/java/no/unit/nva/search/resource/response/Affiliation.java b/search-commons/src/main/java/no/unit/nva/search/resource/response/Affiliation.java index 1cc227295..a19edd4b5 100644 --- a/search-commons/src/main/java/no/unit/nva/search/resource/response/Affiliation.java +++ b/search-commons/src/main/java/no/unit/nva/search/resource/response/Affiliation.java @@ -1,3 +1,13 @@ package no.unit.nva.search.resource.response; -public record Affiliation(String id, String type) {} +import static no.unit.nva.constants.Words.ID; +import static no.unit.nva.constants.Words.TYPE; + +import com.fasterxml.jackson.databind.JsonNode; + +public record Affiliation(String id, String type) { + + public Affiliation(JsonNode node) { + this(node.path(ID).textValue(), node.path(TYPE).textValue()); + } +} diff --git a/search-commons/src/main/java/no/unit/nva/search/resource/response/Identity.java b/search-commons/src/main/java/no/unit/nva/search/resource/response/Identity.java index dc0f221c4..bab85ca16 100644 --- a/search-commons/src/main/java/no/unit/nva/search/resource/response/Identity.java +++ b/search-commons/src/main/java/no/unit/nva/search/resource/response/Identity.java @@ -1,5 +1,18 @@ package no.unit.nva.search.resource.response; +import static no.unit.nva.constants.Words.ID; +import static no.unit.nva.constants.Words.NAME; +import static no.unit.nva.constants.Words.ORC_ID; + +import com.fasterxml.jackson.databind.JsonNode; import java.net.URI; -public record Identity(URI id, String name, URI orcId) {} +public record Identity(URI id, String name, URI orcId) { + + public Identity(JsonNode identity) { + this( + NodeUtils.toUri(identity.path(ID)), + identity.path(NAME).textValue(), + NodeUtils.toUri(identity.path(ORC_ID))); + } +} diff --git a/search-commons/src/main/java/no/unit/nva/search/resource/response/NodeUtils.java b/search-commons/src/main/java/no/unit/nva/search/resource/response/NodeUtils.java new file mode 100644 index 000000000..5cd367171 --- /dev/null +++ b/search-commons/src/main/java/no/unit/nva/search/resource/response/NodeUtils.java @@ -0,0 +1,12 @@ +package no.unit.nva.search.resource.response; + +import com.fasterxml.jackson.databind.JsonNode; +import java.net.URI; +import java.util.Objects; + +public class NodeUtils { + + public static URI toUri(JsonNode node) { + return Objects.isNull(node) || !node.isTextual() ? null : URI.create(node.textValue()); + } +} diff --git a/search-commons/src/main/java/no/unit/nva/search/resource/response/Publisher.java b/search-commons/src/main/java/no/unit/nva/search/resource/response/Publisher.java deleted file mode 100644 index c304f8d7a..000000000 --- a/search-commons/src/main/java/no/unit/nva/search/resource/response/Publisher.java +++ /dev/null @@ -1,5 +0,0 @@ -package no.unit.nva.search.resource.response; - -import java.net.URI; - -public record Publisher(URI id, String name, String scientificValue) {} diff --git a/search-commons/src/main/java/no/unit/nva/search/resource/response/PublishingDetails.java b/search-commons/src/main/java/no/unit/nva/search/resource/response/PublishingDetails.java index 8ac6014d1..c3a0050c2 100644 --- a/search-commons/src/main/java/no/unit/nva/search/resource/response/PublishingDetails.java +++ b/search-commons/src/main/java/no/unit/nva/search/resource/response/PublishingDetails.java @@ -3,4 +3,9 @@ import java.net.URI; public record PublishingDetails( - URI id, String type, Series series, String name, URI doi, Publisher publisher) {} + URI id, + String type, + String name, + URI doi, + ScientificRating series, + ScientificRating publisher) {} diff --git a/search-commons/src/main/java/no/unit/nva/search/resource/response/ResourceSearchResponse.java b/search-commons/src/main/java/no/unit/nva/search/resource/response/ResourceSearchResponse.java index 0872003b0..ec69260b0 100644 --- a/search-commons/src/main/java/no/unit/nva/search/resource/response/ResourceSearchResponse.java +++ b/search-commons/src/main/java/no/unit/nva/search/resource/response/ResourceSearchResponse.java @@ -1,6 +1,12 @@ package no.unit.nva.search.resource.response; +import static no.unit.nva.constants.Words.ENTITY_DESCRIPTION; +import static no.unit.nva.constants.Words.PUBLICATION_INSTANCE; +import static no.unit.nva.constants.Words.REFERENCE; +import static no.unit.nva.constants.Words.TYPE; + import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; import java.net.URI; import java.util.List; import java.util.Map; @@ -20,6 +26,10 @@ public record ResourceSearchResponse( int contributorsCount, PublishingDetails publishingDetails) { + public static Builder responseBuilder() { + return new Builder(); + } + public static final class Builder { private URI id; @@ -36,24 +46,26 @@ public static final class Builder { private int contributorsCount; private PublishingDetails publishingDetails; - public Builder() {} - - public static Builder aResourceSearchResponse() { - return new Builder(); - } + private Builder() {} public Builder withId(URI id) { this.id = id; return this; } - public Builder withIdentifier(String identifier) { - this.identifier = identifier; + public Builder withIdentifier(JsonNode identifier) { + this.identifier = identifier.textValue(); return this; } - public Builder withType(String type) { - this.type = type; + public Builder withType(JsonNode source) { + this.type = + source + .path(ENTITY_DESCRIPTION) + .path(REFERENCE) + .path(PUBLICATION_INSTANCE) + .path(TYPE) + .textValue(); return this; } @@ -67,18 +79,18 @@ public Builder withRecordMetadata(RecordMetadata recordMetadata) { return this; } - public Builder withMainTitle(String mainTitle) { - this.mainTitle = mainTitle; + public Builder withMainTitle(JsonNode mainTitle) { + this.mainTitle = mainTitle.textValue(); return this; } - public Builder withMainLanguageAbstract(String mainLanguageAbstract) { - this.mainLanguageAbstract = mainLanguageAbstract; + public Builder withMainLanguageAbstract(JsonNode mainLanguageAbstract) { + this.mainLanguageAbstract = mainLanguageAbstract.textValue(); return this; } - public Builder withDescription(String description) { - this.description = description; + public Builder withDescription(JsonNode description) { + this.description = description.textValue(); return this; } @@ -97,8 +109,8 @@ public Builder withContributorsPreview(List contributorsPreview) { return this; } - public Builder withContributorsCount(int contributorsCount) { - this.contributorsCount = contributorsCount; + public Builder withContributorsCount(JsonNode contributorsCount) { + this.contributorsCount = contributorsCount.asInt(); return this; } diff --git a/search-commons/src/main/java/no/unit/nva/search/resource/response/ScientificRating.java b/search-commons/src/main/java/no/unit/nva/search/resource/response/ScientificRating.java new file mode 100644 index 000000000..4fc967a51 --- /dev/null +++ b/search-commons/src/main/java/no/unit/nva/search/resource/response/ScientificRating.java @@ -0,0 +1,17 @@ +package no.unit.nva.search.resource.response; + +import static no.unit.nva.constants.Words.ID; +import static no.unit.nva.constants.Words.NAME; +import static no.unit.nva.constants.Words.SCIENTIFIC_VALUE; + +import com.fasterxml.jackson.databind.JsonNode; +import java.net.URI; + +public record ScientificRating(URI id, String name, String scientificValue) { + public ScientificRating(JsonNode series) { + this( + NodeUtils.toUri(series.path(ID)), + series.path(NAME).textValue(), + series.path(SCIENTIFIC_VALUE).textValue()); + } +} diff --git a/search-commons/src/main/java/no/unit/nva/search/resource/response/Series.java b/search-commons/src/main/java/no/unit/nva/search/resource/response/Series.java deleted file mode 100644 index 7b48bb812..000000000 --- a/search-commons/src/main/java/no/unit/nva/search/resource/response/Series.java +++ /dev/null @@ -1,5 +0,0 @@ -package no.unit.nva.search.resource.response; - -import java.net.URI; - -public record Series(URI id, String name, String scientificValue) {} diff --git a/search-commons/src/test/java/no/unit/nva/search/resource/ResourceClientTest.java b/search-commons/src/test/java/no/unit/nva/search/resource/ResourceClientTest.java index ea8eb5a3a..58066d778 100644 --- a/search-commons/src/test/java/no/unit/nva/search/resource/ResourceClientTest.java +++ b/search-commons/src/test/java/no/unit/nva/search/resource/ResourceClientTest.java @@ -7,6 +7,7 @@ import static no.unit.nva.common.MockedHttpResponse.mockedFutureHttpResponse; import static no.unit.nva.common.MockedHttpResponse.mockedHttpResponse; import static no.unit.nva.constants.Words.ALL; +import static no.unit.nva.constants.Words.AUTHORIZATION; import static no.unit.nva.constants.Words.COLON; import static no.unit.nva.constants.Words.COMMA; import static no.unit.nva.constants.Words.CONTRIBUTOR; @@ -58,6 +59,7 @@ import static nva.commons.apigateway.AccessRight.MANAGE_CUSTOMERS; import static nva.commons.apigateway.AccessRight.MANAGE_DEGREE; import static nva.commons.apigateway.AccessRight.MANAGE_RESOURCES_ALL; +import static nva.commons.apigateway.AccessRight.MANAGE_RESOURCES_STANDARD; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; @@ -222,9 +224,10 @@ static Stream uriProvider() { static Stream roleProvider() { return Stream.of( - Arguments.of(5, List.of(MANAGE_RESOURCES_ALL)), - Arguments.of(22, List.of(MANAGE_CUSTOMERS)), - Arguments.of(22, List.of(MANAGE_CUSTOMERS, MANAGE_RESOURCES_ALL))); + Arguments.of(5, List.of(MANAGE_RESOURCES_STANDARD)), + Arguments.of(6, List.of(MANAGE_RESOURCES_ALL)), + Arguments.of(23, List.of(MANAGE_CUSTOMERS)), + Arguments.of(6, List.of(MANAGE_RESOURCES_STANDARD, MANAGE_RESOURCES_ALL))); } static Stream uriSortingProvider() { @@ -275,7 +278,7 @@ private static HttpResponseFormatter fetchDocumentWithId( .fromTestParameterMap(Map.of(ID, indexDocument.getDocumentIdentifier())) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); } @@ -311,11 +314,12 @@ void testingFromRequestInfoSuccessful() throws UnauthorizedException, BadRequest AtomicReference uri = new AtomicReference<>(); uriSortingProvider().findFirst().ifPresent(uri::set); - var accessRights = List.of(MANAGE_CUSTOMERS); + var accessRights = List.of(MANAGE_RESOURCES_STANDARD); var mockedRequestInfoLocal = mock(RequestInfo.class); when(mockedRequestInfoLocal.getPersonAffiliation()) .thenReturn(URI.create("https://api.dev.nva.aws.unit.no/cristin/organization/184.0.0.0")); when(mockedRequestInfoLocal.getAccessRights()).thenReturn(accessRights); + when(mockedRequestInfoLocal.getHeaders()).thenReturn(Map.of(AUTHORIZATION, "Bearer token")); var result = ResourceSearchQuery.builder() @@ -343,7 +347,7 @@ void shouldCheckFacets() throws BadRequestException { .withRequiredParameters(FROM, SIZE, AGGREGATION) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -386,7 +390,7 @@ void userSettingsNotFoundReturn200() .withRequiredParameters(FROM, SIZE) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -415,7 +419,7 @@ void userSettingsNotFoundReturn404() .withRequiredParameters(FROM, SIZE, SORT) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -443,7 +447,7 @@ void userSettingsFailsIOException() .withRequiredParameters(FROM, SIZE) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -472,7 +476,7 @@ void userSettingsFailswithWrongFormat() .withRequiredParameters(FROM, SIZE) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -491,7 +495,7 @@ void emptyResultShouldIncludeHits() throws BadRequestException { .build() .withFilter() .requiredStatus( - NEW, DRAFT, PUBLISHED_METADATA, PUBLISHED, DELETED, UNPUBLISHED, DRAFT_FOR_DELETION) + NEW, DRAFT, UNPUBLISHED, PUBLISHED, DELETED, UNPUBLISHED, DRAFT_FOR_DELETION) .apply() .doSearch(searchClient) .toString(); @@ -527,7 +531,7 @@ void shouldReturnCaseInsensitiveCourses(String searchValue, int expectedHits) .build() .withFilter() .requiredStatus( - NEW, DRAFT, PUBLISHED_METADATA, PUBLISHED, DELETED, UNPUBLISHED, DRAFT_FOR_DELETION) + NEW, DRAFT, UNPUBLISHED, PUBLISHED, DELETED, UNPUBLISHED, DRAFT_FOR_DELETION) .apply() .doSearch(searchClient); @@ -543,6 +547,7 @@ void isSearchingForAllPublicationsAsRoleWork(int expectedHits, List when(requestInfo.getAccessRights()).thenReturn(accessRights); when(requestInfo.getPersonAffiliation()) .thenReturn(URI.create("https://api.dev.nva.aws.unit.no/cristin/organization/20754.0.0.0")); + when(requestInfo.getHeaders()).thenReturn(Map.of(AUTHORIZATION, "Bearer token")); var response = ResourceSearchQuery.builder() @@ -568,6 +573,7 @@ void isSearchingWithWrongAccessFails() throws UnauthorizedException { when(requestInfo.getAccessRights()).thenReturn(List.of(MANAGE_DEGREE)); when(requestInfo.getPersonAffiliation()) .thenReturn(URI.create("https://api.dev.nva.aws.unit.no/cristin/organization/")); + when(requestInfo.getHeaders()).thenReturn(Map.of(AUTHORIZATION, "Bearer token")); assertThrows( UnauthorizedException.class, @@ -585,18 +591,19 @@ void isSearchingWithWrongAccessFails() throws UnauthorizedException { } @Test - void withOrganizationDoWork() throws BadRequestException, UnauthorizedException { + void withIsEditorAndOrganizationDoWork() throws BadRequestException, UnauthorizedException { var requestInfo = mock(RequestInfo.class); - when(requestInfo.getAccessRights()).thenReturn(List.of(MANAGE_CUSTOMERS)); + when(requestInfo.getAccessRights()).thenReturn(List.of(MANAGE_RESOURCES_ALL)); when(requestInfo.getTopLevelOrgCristinId()) .thenReturn( Optional.of( - URI.create("https://api.dev.nva.aws.unit.no/cristin/organization/184.0.0.0"))); + URI.create("https://api.dev.nva.aws.unit.no/cristin/organization/20754.0.0.0"))); + when(requestInfo.getHeaders()).thenReturn(Map.of(AUTHORIZATION, "Bearer token")); + var response = ResourceSearchQuery.builder() .fromRequestInfo(requestInfo) .withDockerHostUri(URI.create(container.getHttpHostAddress())) - .withParameter(NODES_EXCLUDED, META_INFO) .withRequiredParameters(FROM, SIZE) .build() .withFilter() @@ -606,7 +613,7 @@ void withOrganizationDoWork() throws BadRequestException, UnauthorizedException assertNotNull(response); var pagedSearchResourceDto = response.toPagedResponse(); - assertEquals(3, pagedSearchResourceDto.totalHits()); + assertEquals(6, pagedSearchResourceDto.totalHits()); } @Test @@ -650,7 +657,7 @@ void searchWithUriPageableReturnsOpenSearchResponse(URI uri, int expectedCount) .validate() .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -676,7 +683,7 @@ void searchWithUriReturnsOpenSearchAwsResponse(URI uri, int expectedCount) .withDockerHostUri(URI.create(container.getHttpHostAddress())) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -715,7 +722,7 @@ void shouldFindAnthologyWithChapters() throws ApiGatewayException { .withDockerHostUri(URI.create(container.getHttpHostAddress())) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -747,7 +754,7 @@ void shouldFindDocumentsWithParent() throws ApiGatewayException { .withDockerHostUri(URI.create(container.getHttpHostAddress())) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -779,7 +786,7 @@ void shouldFindDocumentsWithChildren() throws ApiGatewayException { .withDockerHostUri(URI.create(container.getHttpHostAddress())) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -818,7 +825,7 @@ void shouldFindChaptersOfBookAnthology() throws ApiGatewayException { .withDockerHostUri(URI.create(container.getHttpHostAddress())) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -874,7 +881,7 @@ void searchUriWithSortingReturnsOpenSearchAwsResponse(URI uri) throws ApiGateway .withDockerHostUri(URI.create(container.getHttpHostAddress())) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -930,7 +937,7 @@ void shouldReturnResourcesForScientificPeriods() throws BadRequestException { .withDockerHostUri(URI.create(container.getHttpHostAddress())) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -952,7 +959,7 @@ void shouldReturnResourcesForSinglePeriods() throws BadRequestException { .withDockerHostUri(URI.create(container.getHttpHostAddress())) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); var pagedSearchResourceDto = response.toPagedResponse(); @@ -979,7 +986,7 @@ void shouldReturnResourcesForSinglePeriods() throws BadRequestException { .withDockerHostUri(URI.create(container.getHttpHostAddress())) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); @@ -1010,7 +1017,7 @@ void shouldReturnResourcesWithSubunitsWhenExcludedSubunitsNotProvided() .withDockerHostUri(URI.create(container.getHttpHostAddress())) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA, DELETED) + .requiredStatus(PUBLISHED, UNPUBLISHED, DELETED) .apply() .doSearch(searchClient); @@ -1021,7 +1028,7 @@ void shouldReturnResourcesWithSubunitsWhenExcludedSubunitsNotProvided() assertThat(pagedSearchResourceDto.toJsonString(), containsString(includedSubunitI)); assertThat(pagedSearchResourceDto.toJsonString(), containsString(includedSubunitII)); - assertThat(pagedSearchResourceDto.hits(), hasSize(2)); + assertThat(pagedSearchResourceDto.hits(), hasSize(3)); } @Test @@ -1034,7 +1041,7 @@ void shouldReturnResourcesWithFieldContributorsPreviewAndNotPreview() throws Bad .withAlwaysExcludedFields("entityDescription.contributors") .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA, DELETED) + .requiredStatus(PUBLISHED, UNPUBLISHED, DELETED) .apply() .doSearch(searchClient); @@ -1056,7 +1063,7 @@ void shouldFilterByPageCount(int min, int max, int expectedResultCount) .withDockerHostUri(URI.create(container.getHttpHostAddress())) .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA) + .requiredStatus(PUBLISHED, UNPUBLISHED) .apply() .doSearch(searchClient); diff --git a/search-commons/src/test/resources/resource_datasource.json b/search-commons/src/test/resources/resource_datasource.json index 1e8543272..874123ef5 100644 --- a/search-commons/src/test/resources/resource_datasource.json +++ b/search-commons/src/test/resources/resource_datasource.json @@ -14,13 +14,13 @@ ], "createdDate": "2023-11-06T08:46:26.204053625Z", "contributorOrganizations": [ - "https://api.dev.nva.aws.unit.no/cristin/organization/20754.0.0.0", - "https://api.dev.nva.aws.unit.no/cristin/organization/20754.6.0.0" + "https://api.dev.nva.aws.unit.no/cristin/organization/20754.6.0.0", + "https://api.dev.nva.aws.unit.no/cristin/organization/20754.0.0.0" ], "entityDescription": { "type": "EntityDescription", "alternativeAbstracts": {}, - "contributorsCount" : 1, + "contributorsCount": 1, "contributors": [ { "type": "Contributor", @@ -117,7 +117,6 @@ "sequence": 1 } ], - "contributorsCount": 1, "mainTitle": "Kjetils ticket test", "language": "http://lexvo.org/id/iso639-3/nor", "publicationDate": { @@ -162,7 +161,7 @@ } } }, - "identifier": "018ba3cfcb9c-94f77a1e-ac36-430a-84b0-0619ecbbaf39", + "identifier": "018ba3cfcb9c-94f77a1e-ac36-430a-84b0-0619e3bbaf39", "modelVersion": "0.20.54", "modifiedDate": "2023-11-06T08:48:11.943422687Z", "nviType": "NonNviCandidate", @@ -402,7 +401,7 @@ "type": "EntityDescription", "abstract": "In this paper, the authors report on collaboration between two universities from 2003 and beyond. The collaborations have been carried out in four main spheres: in the area of Staff training programmes, in the area of Development and research in information literacy, in the area of Developing Models of Digital Repositories and also in the domain of Research in Library Leadership. This cooperation started in 2003 when the authors worked on developing the first Leonardo da Vinci program in the field of continued education, and is still ongoing. The collaborations have been funded by different national and international funding agencies, as well as from the institutions of the authors.", "alternativeAbstracts": {}, - "contributorsCount" : 2, + "contributorsCount": 2, "contributors": [ { "type": "Contributor", @@ -479,7 +478,6 @@ "sequence": 2 } ], - "contributorsCount": 2, "language": "http://lexvo.org/id/iso639-3/eng", "mainTitle": "Digital Library Idé in a Collaborative Context: Romania and Norway 2003-2012", "publicationDate": { @@ -580,7 +578,7 @@ "entityDescription": { "type": "EntityDescription", "alternativeAbstracts": {}, - "contributorsCount" : 1, + "contributorsCount": 1, "contributors": [ { "type": "Contributor", @@ -677,7 +675,6 @@ "sequence": 1 } ], - "contributorsCount": 1, "mainTitle": "Kjetils Idé tickét têst", "abstract": "ønsker deg av hjerte ære berømmelse. Har du en ide om hva du skal gjøre med Kjærringen og eller konå", "language": "http://lexvo.org/id/iso639-3/nor", @@ -1028,7 +1025,7 @@ "type": "EntityDescription", "abstract": "The hazard mapping tool NAKSIN estimates the release probability of potential release areas (PRAs) by testing a stability criterion based on the infinite-slope approximation with a large sample of synthetic weather situations. The release area is thus assumed to comprise the entire PRA, which is unrealistic for avalanches with return periods shorter than 100–300 y. To remedy this, a stability criterion is proposed that accounts for stabilizing forces across the slab perimeter and so is sensitive to the slab extent. The criterion is applied to a sequence of subareas of the PRA with increasing minimum slope angle to find the subarea with maximum release probability. The method is described and formulated mathematically. Also, tools for coding it are suggested but implementation in NAKSIN and testing are left for future work.", "alternativeAbstracts": {}, - "contributorsCount" : 1, + "contributorsCount": 1, "contributors": [ { "type": "Contributor", @@ -1059,7 +1056,6 @@ "sequence": 1 } ], - "contributorsCount": 1, "language": "http://lexvo.org/id/iso639-3/eng", "mainTitle": "A Simple Model for the Ide Variability of Release Area Size", "publicationDate": { @@ -1177,7 +1173,7 @@ "type": "EntityDescription", "abstract": "This Note discusses a quasi-three-dimensional model of mixed snow avalanches that could replace the code MoT-Voellmy in NAKSIN. As a starting point, the two-layer model by Eglit is slightly simplified and extended from a profile line to a general topography in three-dimensional space. Possible modifications of the closures proposed by Eglit are discussed. For rapid development, it is suggested to base the new code either on MoT-Voellmy or possibly on the code development system ExaHyPE.", "alternativeAbstracts": {}, - "contributorsCount" : 1, + "contributorsCount": 1, "contributors": [ { "type": "Contributor", @@ -1208,7 +1204,6 @@ "sequence": 1 } ], - "contributorsCount": 1, "language": "http://lexvo.org/id/iso639-3/eng", "mainTitle": "Idé Outline of a Simple Model of Mixed Snow Avalanches", "publicationDate": { @@ -1276,7 +1271,7 @@ "owner": "ngi@7452.0.0.0", "ownerAffiliation": "https://api.dev.nva.aws.unit.no/cristin/organization/7452.0.0.0" }, - "status": "PUBLISHED_METADATA", + "status": "UNPUBLISHED", "joinField": { "name": "hasParts" } @@ -1314,7 +1309,7 @@ "type": "EntityDescription", "abstract": "To secure linear infrastructure in the most cost-efficient manner, hazard hot-spots need to be known not only with regard to the intensity of possible events but also their probability. Traditional hazard mapping methods rely on analysis of historical records—which are often missing or scarce—or on experts’ subjective judgment. Either approach is timeconsuming and expensive. The hazard mapping system NAKSIN for snow avalanches contains a module for estimating avalanche release probability automatically using topographical, weather and forest data and calculates avalanche run-out for one target return period. This Note outlines how NAKSIN can be modified to produce maps of avalanche hit probability and optionally probability distribution functions of impact pressure and/or flow velocity. While the needed modifications are easy to implement, the NAKSIN module for release probability requires improvements to produce more reliable estimates in areas with continental climate.", "alternativeAbstracts": {}, - "contributorsCount" : 1, + "contributorsCount": 1, "contributors": [ { "type": "Contributor", @@ -1345,7 +1340,6 @@ "sequence": 1 } ], - "contributorsCount": 1, "language": "http://lexvo.org/id/iso639-3/eng", "mainTitle": "«Ide APALI» – Avalanche Probability Along Linear Infrastructure", "publicationDate": { @@ -1454,7 +1448,7 @@ "type": "EntityDescription", "abstract": "During the first year of the project period 2020-2022 of NGIs research project on Snow avalanches, Applied Avalanche Research in Norway (AARN), work was conducted in all three work packages (WP 1 – Avalanche formation and release, WP 2 – Avalanche dynamics, WP 3 – Avalanche interaction) and several cross-package topics. In addition, several significant upgrades were made to the research infrastructure at Fonnbu and Ryggfonn. During 2020, the results of the research activities have been published in peer-reviewed journals, summarised in technical notes, and presented online at national and international conferences and seminars. AARN personnel have been actively engaged in educational outreach activities, including as lecturers and student supervisors.", "alternativeAbstracts": {}, - "contributorsCount" : 10, + "contributorsCount": 10, "contributors": [ { "type": "Contributor", @@ -1699,7 +1693,6 @@ "sequence": 10 } ], - "contributorsCount": 10, "language": "http://lexvo.org/id/iso639-3/eng", "mainTitle": "Annual Report 2020", "publicationDate": { @@ -1792,7 +1785,7 @@ "type": "EntityDescription", "abstract": "During the second year of the project period 2020-2022 of NGIs research project on snow avalanches, Applied Avalanche Research in Norway (AARN), work was conducted in all three work packages (WP 1 – Avalanche formation and release, WP 2 – Avalanche dynamics, WP 3 – Avalanche interaction) and several cross-package topics. The successful avalanche experiment in April 2021 has given us valuable insight into the dynamics of a large avalanche event and showed that the Ryggfonn test site produces the avalanches that are needed for the AARN research. During 2021, the results of the research activities have been published in peer-reviewed journals, summarised in technical notes, and presented online at national and international conferences and seminars. AARN personnel have been actively engaged in educational outreach activities, including as lecturers and student supervisors.", "alternativeAbstracts": {}, - "contributorsCount" : 8, + "contributorsCount": 8, "contributors": [ { "type": "Contributor", @@ -1989,7 +1982,6 @@ "sequence": 7 } ], - "contributorsCount": 8, "language": "http://lexvo.org/id/iso639-3/nor", "mainTitle": "Annual Report 2021", "publicationDate": { @@ -2118,7 +2110,7 @@ "entityDescription": { "type": "EntityDescription", "alternativeAbstracts": {}, - "contributorsCount" : 11, + "contributorsCount": 11, "contributors": [ { "type": "Contributor", @@ -3137,7 +3129,7 @@ "type": "EntityDescription", "abstract": "The Norwegian building code regulates the societal acceptable risk from avalanches for three building classes (S1, S1, and S3). The corresponding highest allowed nominal annual probabilities of avalanches reaching the building for these classes are set as 1/100, 1/1000 and 1/5000 respectively (TEK17, 2017). That is, avalanches should not reach a building, or the accompanying outdoor area and cause (considerable) damage more often than the building class permits. These hazard classes are used for delineation of avalanche hazard zones for land use planning (TEK17, 2017). For the assessment of the quantitative risk of avalanches reaching existing settlements only limited methods are available. Thus, historical observations can be of special importance, as they may be direct indicators for the real hazard in the area of interest. To a certain degree, they can also provide an indication of a possible change of hazard over time due to environmental changes. However, historical observations are affected by inherent uncertainties and many questions remain open. Here, we aim to combine results from several work packages to develop a more consistent method of using historical observations that may help improve quantitative hazard assessments and evaluation of the uncertainties involved.", "alternativeAbstracts": {}, - "contributorsCount" : 1, + "contributorsCount": 1, "contributors": [ { "type": "Contributor", @@ -3265,7 +3257,7 @@ "type": "EntityDescription", "abstract": "Snow avalanches are a significant natural hazard and common phenomenon in Norway. Each year, avalanches result in fatalities, evacuations, and interruptions or damage to infrastructure networks such as roads, railways, and electrical transmission lines. Persistent avalanche hazard in steep terrain is a major factor considered during land-use planning and development. During the snow season, daily or weekly variations in the avalanche danger identified in regional and applied bulletins influence the operation of transportation networks. Applied research on avalanches and their societal impacts has been conducted at the Norwegian Geotechnical Institute (NGI) since 1973. This research has been funded in part by an annual grant from the Norwegian parliament, administered by the Norwegian Water Resources and Energy Directorate (NVE – Norges vassdrags- og energi-direktorat). Recent research activities have improved our understanding of avalanche formation, movement, and impacts. Enhanced knowledge of the individual processes leading to avalanche initiation, avalanche dynamics, and avalanche impacts has been applied to developing tools to help predict avalanche occurrence, runout distance, and impact pressures. While much has been accomplished within the avalanche research community in recent years, many key questions remain. This project plan: (1) presents the research goals for the 2020-2022 period, (2) outlines the projects organization – including potential for external collaboration, and (3) presents the work-package structure and specific research tasks to be undertaken by the applied avalanche research group at NGI over the next three years.", "alternativeAbstracts": {}, - "contributorsCount" : 7, + "contributorsCount": 7, "contributors": [ { "type": "Contributor", @@ -3498,7 +3490,6 @@ "sequence": 4 } ], - "contributorsCount": 7, "language": "http://lexvo.org/id/iso639-3/eng", "mainTitle": "Research Plan 2020–2022", "publicationDate": { @@ -3749,7 +3740,7 @@ "alternativeTitles": { "en": "Validity of the NTNU Prediction Model for D&B Tunnelling" }, - "contributorsCount" : 4, + "contributorsCount": 4, "contributors": [ { "type": "Contributor", @@ -4187,7 +4178,7 @@ "alternativeTitles": { "en": "Time series modelling: applications for groundwater control in urban tunnelling" }, - "contributorsCount" : 4, + "contributorsCount": 4, "contributors": [ { "type": "Contributor", @@ -4344,7 +4335,6 @@ "sequence": 2 } ], - "contributorsCount": 4, "language": "http://lexvo.org/id/iso639-3/eng", "mainTitle": "Time series modelling: applications for groundwater control in urban tunnelling", "publicationDate": { @@ -4664,7 +4654,7 @@ "type": "EntityDescription", "abstract": "(Abstract): This chapter's point of departure is that business problems too often are approached in a very narrow manner, due to what we call the inclination of techno-centrism. It involves that we may ignore solutions that are better and more sustainable in the long run. Three additional dimensions for formulation and resolution of complex business-related decision problems are explore, and we argue that these are necessary as complements to the technical dimension - (1) the systemic dimension, (2) the social dimension and (3) the existential dimension. We argue that ethical reflection may create more awareness around these problems and as a consequence be part of the solution. Finally, we present the outline of a new course in business ethics that aims to counterbalance the techno-centricity in contemporary business education, and hopefully this may be seen as a new direction for business ethics along the lines of student-centricity and positive psychology, which may improve the education of business students.", "alternativeAbstracts": {}, - "contributorsCount" : 3, + "contributorsCount": 3, "contributors": [ { "type": "Contributor", @@ -4751,7 +4741,6 @@ "sequence": 3 } ], - "contributorsCount": 3, "language": "http://lexvo.org/id/iso639-3/eng", "mainTitle": "The Solution: Business Ethics! But what was the problem?", "publicationDate": { @@ -4893,7 +4882,7 @@ "entityDescription": { "type": "EntityDescription", "alternativeAbstracts": {}, - "contributorsCount" : 6, + "contributorsCount": 6, "contributors": [ { "type": "Contributor", diff --git a/search-handlers/src/main/java/no/unit/nva/search/SearchResourceAuthHandler.java b/search-handlers/src/main/java/no/unit/nva/search/SearchResourceAuthHandler.java index 4f2c78ecb..d32f13f73 100644 --- a/search-handlers/src/main/java/no/unit/nva/search/SearchResourceAuthHandler.java +++ b/search-handlers/src/main/java/no/unit/nva/search/SearchResourceAuthHandler.java @@ -1,10 +1,6 @@ package no.unit.nva.search; import static no.unit.nva.constants.Defaults.DEFAULT_RESPONSE_MEDIA_TYPES; -import static no.unit.nva.search.common.enums.PublicationStatus.DELETED; -import static no.unit.nva.search.common.enums.PublicationStatus.PUBLISHED; -import static no.unit.nva.search.common.enums.PublicationStatus.PUBLISHED_METADATA; -import static no.unit.nva.search.common.enums.PublicationStatus.UNPUBLISHED; import static no.unit.nva.search.resource.Constants.V_2024_12_01_SIMPLER_MODEL; import static no.unit.nva.search.resource.ResourceClient.defaultClient; import static no.unit.nva.search.resource.ResourceParameter.AGGREGATION; @@ -73,7 +69,6 @@ protected String processInput(Void input, RequestInfo requestInfo, Context conte .validate() .build() .withFilter() - .requiredStatus(PUBLISHED, PUBLISHED_METADATA, DELETED, UNPUBLISHED) .customerCurationInstitutions(requestInfo) .apply() .doSearch(opensearchClient) @@ -86,9 +81,18 @@ protected Integer getSuccessStatusCode(Void input, String output) { return HttpURLConnection.HTTP_OK; } + /** + * Validates that the user has the required access rights to search resources. + * MANAGE_RESOURCES_ALL (editor) MANAGE_RESOURCES_STANDARD (any curator) MANAGE_CUSTOMERS (app + * admin) + * + * @param accessRights the access rights of the user. + * @throws UnauthorizedException if the user does not have the required access rights. + */ private void validateAccessRight(List accessRights) throws UnauthorizedException { if (accessRights.contains(AccessRight.MANAGE_RESOURCES_ALL) - || accessRights.contains(AccessRight.MANAGE_CUSTOMERS)) { + || accessRights.contains(AccessRight.MANAGE_CUSTOMERS) + || accessRights.contains(AccessRight.MANAGE_RESOURCES_STANDARD)) { return; } throw new UnauthorizedException(); diff --git a/search-handlers/src/test/java/no/unit/nva/search/SearchResourceAuthHandlerTest.java b/search-handlers/src/test/java/no/unit/nva/search/SearchResourceAuthHandlerTest.java index 8bad18ed6..013e409b8 100644 --- a/search-handlers/src/test/java/no/unit/nva/search/SearchResourceAuthHandlerTest.java +++ b/search-handlers/src/test/java/no/unit/nva/search/SearchResourceAuthHandlerTest.java @@ -124,6 +124,7 @@ private InputStream getInputStreamWithAccessRight( .withCurrentCustomer(currentCustomer) .withTopLevelCristinOrgId(topLevelCristinOrgId) .withAccessRights(currentCustomer, accessRight) + .withHeaders(Map.of("Authorization", "Bearer " + randomString())) .build(); }