diff --git a/.github/workflows/app.dolly-search-service.yml b/.github/workflows/app.dolly-search-service.yml new file mode 100644 index 00000000000..65f242870f3 --- /dev/null +++ b/.github/workflows/app.dolly-search-service.yml @@ -0,0 +1,25 @@ +name: dolly-search-service + +on: + push: + paths: + - "plugins/**" + - "libs/data-transfer-search-objects/**" + - "libs/reactive-core/**" + - "libs/security-core/**" + - "libs/servlet-core/**" + - "libs/servlet-security/**" + - "libs/testing/**" + - "apps/dolly-search-service/**" + - ".github/workflows/app.dolly-search-service.yml" + +jobs: + workflow: + uses: ./.github/workflows/common.workflow.backend.yml + with: + working-directory: "apps/dolly-search-service" + deploy-tag: "#deploy-dolly-search-service" + permissions: + contents: read + id-token: write + secrets: inherit diff --git a/apps/dolly-backend/config.test.yml b/apps/dolly-backend/config.test.yml index 4b0375689e7..ab6f3de696c 100644 --- a/apps/dolly-backend/config.test.yml +++ b/apps/dolly-backend/config.test.yml @@ -32,6 +32,7 @@ spec: - application: testnav-arbeidsforhold-service - application: testnav-arbeidsplassencv-proxy - application: testnav-arbeidssoekerregisteret-proxy + - application: testnav-dolly-search-service - application: testnav-inntektsmelding-service - application: testnav-kodeverk-service - application: testnav-miljoer-service diff --git a/apps/dolly-backend/config.yml b/apps/dolly-backend/config.yml index 1175b294d1c..30963ba28a8 100644 --- a/apps/dolly-backend/config.yml +++ b/apps/dolly-backend/config.yml @@ -17,15 +17,16 @@ spec: accessPolicy: inbound: rules: + - application: dolly-frontend - application: dolly-idporten + - application: etterlatte-testdata + namespace: etterlatte - application: testnav-batch-bestilling-service - application: testnav-dollystatus - application: testnav-helsepersonell-service - application: testnav-oversikt-frontend - application: testnav-tenor-search-service - - application: etterlatte-testdata - namespace: etterlatte outbound: rules: - application: generer-navn-service @@ -33,6 +34,7 @@ spec: - application: testnav-arbeidsforhold-service - application: testnav-arbeidsplassencv-proxy - application: testnav-arbeidssoekerregisteret-proxy + - application: testnav-dolly-search-service - application: testnav-inntektsmelding-service - application: testnav-kodeverk-service - application: testnav-miljoer-service diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/DollyBackendApplicationStarter.java b/apps/dolly-backend/src/main/java/no/nav/dolly/DollyBackendApplicationStarter.java index 49a646b9734..bc4c91d31b3 100644 --- a/apps/dolly-backend/src/main/java/no/nav/dolly/DollyBackendApplicationStarter.java +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/DollyBackendApplicationStarter.java @@ -12,4 +12,4 @@ public static void main(String[] args) { .initializers(new NaisEnvironmentApplicationContextInitializer()) .run(args); } -} +} \ No newline at end of file diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/config/Consumers.java b/apps/dolly-backend/src/main/java/no/nav/dolly/config/Consumers.java index eef10a7ed84..aaf5f1c51a0 100644 --- a/apps/dolly-backend/src/main/java/no/nav/dolly/config/Consumers.java +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/config/Consumers.java @@ -47,4 +47,5 @@ public class Consumers { private ServerProperties testnavSkattekortService; private ServerProperties yrkesskadeProxy; private ServerProperties arbeidssoekerregisteretProxy; + private ServerProperties dollySearchService; } diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/elastic/dto/SearchResponse.java b/apps/dolly-backend/src/main/java/no/nav/dolly/elastic/dto/SearchResponse.java index 87b68865e55..da5d61a1464 100644 --- a/apps/dolly-backend/src/main/java/no/nav/dolly/elastic/dto/SearchResponse.java +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/elastic/dto/SearchResponse.java @@ -7,8 +7,11 @@ import lombok.NoArgsConstructor; import no.nav.dolly.elastic.ElasticBestilling; +import java.util.ArrayList; import java.util.List; +import static java.util.Objects.isNull; + @Data @Builder @NoArgsConstructor @@ -20,6 +23,22 @@ public class SearchResponse { private Float score; private String took; private List identer; - private List bestillinger; + private List registre; private String error; + + public List getIdenter() { + + if (isNull(identer)) { + identer = new ArrayList<>(); + } + return identer; + } + + public List getRegistre() { + + if (isNull(registre)) { + registre = new ArrayList<>(); + } + return registre; + } } \ No newline at end of file diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/elastic/service/OpenSearchQueryBuilder.java b/apps/dolly-backend/src/main/java/no/nav/dolly/elastic/service/OpenSearchQueryBuilder.java index 85474cc3d36..0789b535f9c 100644 --- a/apps/dolly-backend/src/main/java/no/nav/dolly/elastic/service/OpenSearchQueryBuilder.java +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/elastic/service/OpenSearchQueryBuilder.java @@ -70,7 +70,7 @@ public static BoolQueryBuilder buildTyperQuery(ElasticTyper[] typer) { return queryBuilder; } - private void setPersonQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + private static void setPersonQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { Optional.ofNullable(request.getPersonRequest()) .ifPresent(value -> { @@ -107,7 +107,7 @@ private void setPersonQuery(BoolQueryBuilder queryBuilder, SearchRequest request }); } - private QueryBuilder getFagsystemQuery(ElasticTyper type) { + public static QueryBuilder getFagsystemQuery(ElasticTyper type) { return switch (type) { case AAREG -> QueryBuilders.existsQuery("aareg"); diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/DollySearchServiceConsumer.java b/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/DollySearchServiceConsumer.java new file mode 100644 index 00000000000..387caf04423 --- /dev/null +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/DollySearchServiceConsumer.java @@ -0,0 +1,39 @@ +package no.nav.dolly.opensearch; + +import lombok.extern.slf4j.Slf4j; +import no.nav.dolly.config.Consumers; +import no.nav.dolly.opensearch.command.DollySearchServicePostCommand; +import no.nav.testnav.libs.data.dollysearchservice.v1.SearchRequest; +import no.nav.testnav.libs.data.dollysearchservice.v1.SearchResponse; +import no.nav.testnav.libs.securitycore.domain.ServerProperties; +import no.nav.testnav.libs.standalone.servletsecurity.exchange.TokenExchange; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Service +@Slf4j +public class DollySearchServiceConsumer { + + private final TokenExchange tokenService; + private final WebClient webClient; + private final ServerProperties serverProperties; + + public DollySearchServiceConsumer( + TokenExchange tokenService, + Consumers consumers, + WebClient.Builder webClientBuilder) { + + this.tokenService = tokenService; + serverProperties = consumers.getDollySearchService(); + this.webClient = webClientBuilder + .baseUrl(serverProperties.getUrl()) + .build(); + } + + public Mono doPersonSearch(SearchRequest request) { + + return tokenService.exchange(serverProperties) + .flatMap(token -> new DollySearchServicePostCommand(webClient, request, token.getTokenValue()).call()); + } +} diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/command/DollySearchServicePostCommand.java b/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/command/DollySearchServicePostCommand.java new file mode 100644 index 00000000000..a3a789c1110 --- /dev/null +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/command/DollySearchServicePostCommand.java @@ -0,0 +1,40 @@ +package no.nav.dolly.opensearch.command; + +import lombok.RequiredArgsConstructor; +import no.nav.testnav.libs.data.dollysearchservice.v1.SearchRequest; +import no.nav.testnav.libs.data.dollysearchservice.v1.SearchResponse; +import no.nav.testnav.libs.reactivecore.utils.WebClientFilter; +import org.springframework.http.HttpHeaders; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +import java.time.Duration; +import java.util.concurrent.Callable; + +@RequiredArgsConstructor +public class DollySearchServicePostCommand implements Callable> { + + private static final String SEARCH_URL = "/api/v1/opensearch"; + + private final WebClient webClient; + private final SearchRequest request; + private final String token; + + @Override + public Mono call() { + + return webClient.post() + .uri(uriBuilder -> uriBuilder.path(SEARCH_URL).build()) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .bodyValue(request) + .retrieve() + .bodyToMono(SearchResponse.class) + .doOnError(WebClientFilter::logErrorMessage) + .retryWhen(Retry.backoff(3, Duration.ofSeconds(5)) + .filter(WebClientFilter::is5xxException)) + .onErrorResume(error -> Mono.just(SearchResponse.builder() + .error(WebClientFilter.getMessage(error)) + .build())); + } +} diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/dto/SearchRequest.java b/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/dto/SearchRequest.java new file mode 100644 index 00000000000..fb50e13c2d0 --- /dev/null +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/dto/SearchRequest.java @@ -0,0 +1,52 @@ +package no.nav.dolly.opensearch.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import no.nav.testnav.libs.data.dollysearchservice.v1.PersonRequest; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SearchRequest { + + @Schema(description = "Paginering for bestillinger") + private PagineringBestillingRequest pagineringBestillingRequest; + + @Schema(description = "Paginering for personersøk") + private PagineringPersonRequest pagineringPersonRequest; + + @Schema(description = "Persondetaljer") + private PersonRequest personRequest; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class PagineringBestillingRequest { + + @Schema(description = "Seed for paginering") + private Integer seed; + @Schema(description = "Sidenummer") + private Integer side; + @Schema(description = "Antall resultater per side") + private Integer antall; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class PagineringPersonRequest { + + @Schema(description = "Sidenummer") + private Integer side; + @Schema(description = "Antall resultater per side") + private Integer antall; + @Schema(description = "Seed for paginering") + private Integer seed; + } +} diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/dto/SearchResponse.java b/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/dto/SearchResponse.java new file mode 100644 index 00000000000..6f0b7de9c98 --- /dev/null +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/dto/SearchResponse.java @@ -0,0 +1,39 @@ +package no.nav.dolly.opensearch.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import net.minidev.json.annotate.JsonIgnore; +import no.nav.dolly.elastic.ElasticTyper; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SearchResponse { + + private RegistreResponseStatus registreSearchResponse; + private no.nav.testnav.libs.data.dollysearchservice.v1.SearchResponse dollySearchResponse; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RegistreResponseStatus { + + private Long totalHitsBestillinger; + private Float score; + private String took; + private Integer antall; + private Integer side; + private Integer antallIdenter; + private Integer seed; + private List registre; + private String error; + @JsonIgnore + private List identer; + } +} diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/mapper/OpenSearchRequestMappingStrategy.java b/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/mapper/OpenSearchRequestMappingStrategy.java new file mode 100644 index 00000000000..de62113745e --- /dev/null +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/mapper/OpenSearchRequestMappingStrategy.java @@ -0,0 +1,29 @@ +package no.nav.dolly.opensearch.mapper; + +import ma.glasnost.orika.CustomMapper; +import ma.glasnost.orika.MapperFactory; +import ma.glasnost.orika.MappingContext; +import no.nav.dolly.mapper.MappingStrategy; +import no.nav.dolly.opensearch.dto.SearchRequest; +import org.springframework.stereotype.Component; + +@Component +public class OpenSearchRequestMappingStrategy implements MappingStrategy { + + @Override + public void register(MapperFactory factory) { + + factory.classMap(SearchRequest.class, no.nav.testnav.libs.data.dollysearchservice.v1.SearchRequest.class) + .customize(new CustomMapper<>() { + @Override + public void mapAtoB(SearchRequest kilde, no.nav.testnav.libs.data.dollysearchservice.v1.SearchRequest destinasjon, MappingContext context) { + + destinasjon.setAntall(kilde.getPagineringPersonRequest().getAntall()); + destinasjon.setSide(kilde.getPagineringPersonRequest().getSide()); + destinasjon.setSeed(kilde.getPagineringPersonRequest().getSeed()); + } + }) + .byDefault() + .register(); + } +} diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/service/DollySearchService.java b/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/service/DollySearchService.java new file mode 100644 index 00000000000..0533ac542e1 --- /dev/null +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/opensearch/service/DollySearchService.java @@ -0,0 +1,138 @@ +package no.nav.dolly.opensearch.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import ma.glasnost.orika.MapperFacade; +import no.nav.dolly.elastic.ElasticTyper; +import no.nav.dolly.elastic.service.OpenSearchQueryBuilder; +import no.nav.dolly.opensearch.DollySearchServiceConsumer; +import no.nav.dolly.opensearch.dto.SearchRequest; +import no.nav.dolly.opensearch.dto.SearchResponse; +import org.opensearch.client.RequestOptions; +import org.opensearch.client.RestHighLevelClient; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.index.query.functionscore.RandomScoreFunctionBuilder; +import org.opensearch.search.SearchHit; +import org.opensearch.search.SearchHits; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Random; + +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DollySearchService { + + private static final Random SEED = new SecureRandom(); + + private final RestHighLevelClient restHighLevelClient; + private final DollySearchServiceConsumer dollySearchServiceConsumer; + private final MapperFacade mapperFacade; + + @Value("${open.search.index}") + private String index; + + public Mono search(List registre, SearchRequest request) { + + var personRequest = mapperFacade.map(request, no.nav.testnav.libs.data.dollysearchservice.v1.SearchRequest.class); + var response = new SearchResponse(); + + var registreResponse = execRegistreQuery(registre, request); + response.setRegistreSearchResponse(registreResponse); + personRequest.setIdenter(new HashSet<>(!registreResponse.getIdenter().isEmpty() ? + registreResponse.getIdenter() : List.of("99999999999"))); + + return dollySearchServiceConsumer.doPersonSearch(personRequest) + .map(personResultat -> { + response.setDollySearchResponse(personResultat); + response.getRegistreSearchResponse().setIdenter(null); + return response; + }); + } + + private SearchResponse.RegistreResponseStatus execRegistreQuery(List registre, SearchRequest request) { + + var side = isNull(request.getPagineringBestillingRequest().getSide()) ? + 1 : request.getPagineringBestillingRequest().getSide(); + var antall = isNull(request.getPagineringBestillingRequest().getAntall()) ? + 1000 : request.getPagineringBestillingRequest().getAntall(); + var seed = isNull(request.getPagineringBestillingRequest().getSeed()) ? + SEED.nextInt() : request.getPagineringBestillingRequest().getSeed(); + + var query = buildTyperQuery(registre, seed); + var searchRequest = new org.opensearch.action.search.SearchRequest(index); + searchRequest + .source(new SearchSourceBuilder().query(query) + .size(antall) + .from(side)); + + try { + var registerResultat = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); + var registreResponse = getIdenter(registerResultat); + registreResponse.setRegistre(registre); + registreResponse.setSide(side); + registreResponse.setAntall(antall); + registreResponse.setSeed(seed); + + return registreResponse; + + } catch (IOException e) { + log.error("OpenSearch feil ved utføring av søk: {}", e.getMessage(), e); + return SearchResponse.RegistreResponseStatus.builder() + .error(e.getLocalizedMessage()) + .build(); + } + } + + private static SearchResponse.RegistreResponseStatus getIdenter(org.opensearch.action.search.SearchResponse response) { + + var identer = Arrays.stream(response.getHits().getHits()) + .map(SearchHit::getSourceAsMap) + .map(map -> (List) map.get("identer")) + .flatMap(Collection::stream) + .distinct() + .toList(); + return SearchResponse.RegistreResponseStatus.builder() + .identer(identer) + .totalHitsBestillinger(getTotalHits(response.getHits())) + .took(response.getTook().getStringRep()) + .antallIdenter(identer.size()) + .score(response.getHits().getMaxScore()) + .build(); + } + + @SuppressWarnings("java:S2259") + private static Long getTotalHits(SearchHits searchHits) { + + return nonNull(searchHits) && nonNull(searchHits.getTotalHits()) ? + searchHits.getTotalHits().value : null; + } + + private static BoolQueryBuilder buildTyperQuery(List typer, Integer seed) { + + var queryBuilder = QueryBuilders.boolQuery() + .must(QueryBuilders.functionScoreQuery( + new RandomScoreFunctionBuilder().seed(seed))); + + if (nonNull(typer)) { + typer.stream() + .map(OpenSearchQueryBuilder::getFagsystemQuery) + .forEach(queryBuilder::must); + } + + return queryBuilder; + } +} diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/provider/api/DollySearchController.java b/apps/dolly-backend/src/main/java/no/nav/dolly/provider/api/DollySearchController.java new file mode 100644 index 00000000000..ab9642609f3 --- /dev/null +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/provider/api/DollySearchController.java @@ -0,0 +1,30 @@ +package no.nav.dolly.provider.api; + +import lombok.RequiredArgsConstructor; +import no.nav.dolly.opensearch.dto.SearchRequest; +import no.nav.dolly.opensearch.dto.SearchResponse; +import no.nav.dolly.elastic.ElasticTyper; +import no.nav.dolly.opensearch.service.DollySearchService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/opensearch") +@RequiredArgsConstructor +public class DollySearchController { + + private final DollySearchService dollySearchService; + + @PostMapping + public Mono searchPersoner(@RequestParam(required = false) List registre, + @RequestBody SearchRequest request) { + + return dollySearchService.search(registre, request); + } +} diff --git a/apps/dolly-backend/src/main/resources/application-local.yml b/apps/dolly-backend/src/main/resources/application-local.yml index b1da061c9a8..7403cba6d51 100644 --- a/apps/dolly-backend/src/main/resources/application-local.yml +++ b/apps/dolly-backend/src/main/resources/application-local.yml @@ -63,4 +63,6 @@ consumers: yrkesskade-proxy: url: https://testnav-yrkesskade-proxy.intern.dev.nav.no arbeidssoekerregisteret-proxy: - url: https://testnav-arbeidssoekerregisteret-proxy.intern.dev.nav.no \ No newline at end of file + url: https://testnav-arbeidssoekerregisteret-proxy.intern.dev.nav.no + dolly-search-service: + url: https://testnav-dolly-search-service.intern.dev.nav.no \ No newline at end of file diff --git a/apps/dolly-backend/src/main/resources/application.yml b/apps/dolly-backend/src/main/resources/application.yml index c81b075d146..6e8bfedc134 100644 --- a/apps/dolly-backend/src/main/resources/application.yml +++ b/apps/dolly-backend/src/main/resources/application.yml @@ -250,4 +250,9 @@ consumers: namespace: dolly url: http://testnav-arbeidssoekerregisteret-proxy.dolly.svc.cluster.local cluster: dev-gcp + dolly-search-service: + name: testnav-dolly-search-service + namespace: dolly + url: http://testnav-dolly-search-service.dolly.svc.cluster.local + cluster: dev-gcp \ No newline at end of file diff --git a/apps/dolly-search-service/Dockerfile b/apps/dolly-search-service/Dockerfile new file mode 100644 index 00000000000..4a36f93546f --- /dev/null +++ b/apps/dolly-search-service/Dockerfile @@ -0,0 +1,8 @@ +FROM ghcr.io/navikt/baseimages/temurin:21 +LABEL maintainer="Team Dolly" + +ENV JAVA_OPTS="-Dspring.profiles.active=prod" + +ADD /build/libs/app.jar /app/app.jar + +EXPOSE 8080 diff --git a/apps/dolly-search-service/README.md b/apps/dolly-search-service/README.md new file mode 100644 index 00000000000..ba17757db99 --- /dev/null +++ b/apps/dolly-search-service/README.md @@ -0,0 +1,8 @@ +# testnav-dolly-search-service +Service som forvalter søking på personer basert på innsendte kriterier + +## Lokal kjøring +* [Generelt.](../../docs/local_general.md) +* [Secret Manager.](../../docs/local_secretmanager.md) +* [OpenSearch.](../../docs/local_opensearch.md) + diff --git a/apps/dolly-search-service/build.gradle b/apps/dolly-search-service/build.gradle new file mode 100644 index 00000000000..bcc3647ab12 --- /dev/null +++ b/apps/dolly-search-service/build.gradle @@ -0,0 +1,29 @@ +plugins { + id "dolly-apps" +} + +sonarqube { + properties { + property "sonar.projectKey", "dolly-search-service" + property "sonar.projectName", "dolly-search-service" + } +} + +dependencies { + implementation "no.nav.testnav.libs:data-transfer-search-objects" + implementation "no.nav.testnav.libs:reactive-core" + implementation "no.nav.testnav.libs:security-core" + implementation "no.nav.testnav.libs:servlet-core" + implementation "no.nav.testnav.libs:servlet-security" + implementation "no.nav.testnav.libs:testing" + + implementation "org.opensearch.client:spring-data-opensearch:$versions.opensearch" + + implementation "com.fasterxml.jackson.core:jackson-core:$versions.jackson" + + implementation "org.springframework.boot:spring-boot-starter-security" + implementation "org.springframework.boot:spring-boot-starter-oauth2-client" + implementation "org.springframework.boot:spring-boot-starter-web" + + implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:$versions.springdoc" +} \ No newline at end of file diff --git a/apps/dolly-search-service/config.yml b/apps/dolly-search-service/config.yml new file mode 100644 index 00000000000..57e67706d11 --- /dev/null +++ b/apps/dolly-search-service/config.yml @@ -0,0 +1,64 @@ +apiVersion: "nais.io/v1alpha1" +kind: "Application" +metadata: + name: testnav-dolly-search-service + namespace: dolly + labels: + team: dolly + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: "8m" + nginx.ingress.kubernetes.io/proxy-buffer-size: "8m" + nginx.ingress.kubernetes.io/client-body-buffer-size: "8m" +spec: + image: "{{image}}" + port: 8080 + webproxy: true + tokenx: + enabled: true + azure: + application: + allowAllUsers: true + enabled: true + tenant: nav.no + accessPolicy: + inbound: + rules: + - application: dolly-backend + - application: dolly-backend-dev + - application: team-dolly-lokal-app + - application: testnav-oversikt-frontend + outbound: + external: + - host: testnav-pdl-proxy.dev-fss-pub.nais.io + liveness: + path: /internal/isAlive + initialDelay: 4 + periodSeconds: 5 + failureThreshold: 500 + observability: + logging: + destinations: + - id: elastic + autoInstrumentation: + enabled: true + runtime: java + readiness: + path: /internal/isReady + initialDelay: 4 + periodSeconds: 5 + failureThreshold: 500 + prometheus: + enabled: true + path: /internal/metrics + replicas: + min: 1 + max: 1 + resources: + requests: + cpu: 200m + memory: 1024Mi + limits: + memory: 2048Mi + ingresses: + - "https://testnav-dolly-search-service.intern.dev.nav.no" + diff --git a/apps/dolly-search-service/gradle/wrapper/gradle-wrapper.jar b/apps/dolly-search-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..e6441136f3d Binary files /dev/null and b/apps/dolly-search-service/gradle/wrapper/gradle-wrapper.jar differ diff --git a/apps/dolly-search-service/gradle/wrapper/gradle-wrapper.properties b/apps/dolly-search-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..a4413138c96 --- /dev/null +++ b/apps/dolly-search-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/apps/dolly-search-service/gradlew b/apps/dolly-search-service/gradlew new file mode 100755 index 00000000000..b740cf13397 --- /dev/null +++ b/apps/dolly-search-service/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/apps/dolly-search-service/gradlew.bat b/apps/dolly-search-service/gradlew.bat new file mode 100644 index 00000000000..25da30dbdee --- /dev/null +++ b/apps/dolly-search-service/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/apps/dolly-search-service/gradlewUpdate.sh b/apps/dolly-search-service/gradlewUpdate.sh new file mode 100755 index 00000000000..e5ee6361152 --- /dev/null +++ b/apps/dolly-search-service/gradlewUpdate.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +gradle wrapper \ No newline at end of file diff --git a/apps/dolly-search-service/settings.gradle b/apps/dolly-search-service/settings.gradle new file mode 100644 index 00000000000..9ebb65a2187 --- /dev/null +++ b/apps/dolly-search-service/settings.gradle @@ -0,0 +1,21 @@ +plugins { + id "com.gradle.develocity" version "3.17.4" +} + +rootProject.name = 'dolly-search-service' + +includeBuild "../../plugins/java" + +includeBuild '../../libs/data-transfer-search-objects' +includeBuild '../../libs/reactive-core' +includeBuild '../../libs/security-core' +includeBuild '../../libs/servlet-core' +includeBuild '../../libs/servlet-security' +includeBuild '../../libs/testing' + +develocity { + buildScan { + termsOfUseUrl = "https://gradle.com/terms-of-service" + termsOfUseAgree = "yes" + } +} \ No newline at end of file diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/DollySearchServiceApplicationStarter.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/DollySearchServiceApplicationStarter.java new file mode 100644 index 00000000000..1ed1004ae1f --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/DollySearchServiceApplicationStarter.java @@ -0,0 +1,16 @@ +package no.nav.testnav.dollysearchservice; + +import no.nav.dolly.libs.nais.NaisEnvironmentApplicationContextInitializer; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; + +@SpringBootApplication +public class +DollySearchServiceApplicationStarter { + public static void main(String[] args) { + + new SpringApplicationBuilder(DollySearchServiceApplicationStarter.class) + .initializers(new NaisEnvironmentApplicationContextInitializer()) + .run(args); + } +} \ No newline at end of file diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/ApplicationConfig.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/ApplicationConfig.java new file mode 100644 index 00000000000..7a1cba2632c --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/ApplicationConfig.java @@ -0,0 +1,15 @@ +package no.nav.testnav.dollysearchservice.config; + +import no.nav.testnav.libs.servletcore.config.ApplicationCoreConfig; +import no.nav.testnav.libs.servletsecurity.jwt.SecureOAuth2ServerToServerAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({ + ApplicationCoreConfig.class, + SecureOAuth2ServerToServerAutoConfiguration.class +}) +public class ApplicationConfig { + +} diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/Consumers.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/Consumers.java new file mode 100644 index 00000000000..7f6ff695b81 --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/Consumers.java @@ -0,0 +1,20 @@ +package no.nav.testnav.dollysearchservice.config; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import no.nav.testnav.libs.securitycore.domain.ServerProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import static lombok.AccessLevel.PACKAGE; + +@Configuration +@ConfigurationProperties(prefix = "consumers") +@NoArgsConstructor(access = PACKAGE) +@Getter +@Setter(PACKAGE) +public class Consumers { + + private ServerProperties testnavPdlProxy; +} diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/JsonMapperConfig.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/JsonMapperConfig.java new file mode 100644 index 00000000000..3a66ddb2cc9 --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/JsonMapperConfig.java @@ -0,0 +1,82 @@ +package no.nav.testnav.dollysearchservice.config; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.ZonedDateTimeSerializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +@Configuration +public class JsonMapperConfig { + + @Bean + public ObjectMapper objectMapper() { + + SimpleModule simpleModule = new SimpleModule() + .addDeserializer(LocalDateTime.class, new DollyLocalDateTimeDeserializer()) + .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ISO_DATE_TIME)) + .addDeserializer(LocalDate.class, new DollyLocalDateDeserializer()) + .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_DATE)) + .addDeserializer(ZonedDateTime.class, new DollyZonedDateTimeDeserializer()) + .addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer(DateTimeFormatter.ISO_DATE_TIME)); + return JsonMapper + .builder() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true) + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + .build() + .registerModule(simpleModule); + + } + + private static class DollyZonedDateTimeDeserializer extends JsonDeserializer { + + @Override + public ZonedDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + if (isBlank(node.asText())) { + return null; + } + return ZonedDateTime.parse(node.asText(), DateTimeFormatter.ISO_DATE_TIME); + } + } + + private static class DollyLocalDateDeserializer extends JsonDeserializer { + + @Override + public LocalDate deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + if (isBlank(node.asText())) { + return null; + } + String dateTime = node.asText().length() > 10 ? node.asText().substring(0, 10) : node.asText(); + return LocalDate.parse(dateTime); + } + } + + private static class DollyLocalDateTimeDeserializer extends JsonDeserializer { + + @Override + public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + if (isBlank(node.asText())) { + return null; + } + String dateTime = node.asText().length() > 19 ? node.asText().substring(0, 19) : node.asText(); + return dateTime.length() > 10 ? LocalDateTime.parse(dateTime) : LocalDate.parse(dateTime).atStartOfDay(); + } + } +} diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/OpenApiConfig.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/OpenApiConfig.java new file mode 100644 index 00000000000..bee18b431ed --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/OpenApiConfig.java @@ -0,0 +1,68 @@ +package no.nav.testnav.dollysearchservice.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.Arrays; + +import no.nav.testnav.libs.securitycore.config.UserConstant; +import no.nav.testnav.libs.servletcore.config.ApplicationProperties; + +@Configuration +public class OpenApiConfig implements WebMvcConfigurer { + + @Bean + public OpenAPI openApi(ApplicationProperties applicationProperties) { + return new OpenAPI() + .components(new Components() + .addSecuritySchemes("bearer-jwt", new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .in(SecurityScheme.In.HEADER) + .name("Authorization") + .description("Trenger ikke \"Bearer \" foran") + ) + .addSecuritySchemes("user-jwt", new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .scheme("bearer") + .bearerFormat("JWT") + .in(SecurityScheme.In.HEADER) + .name(UserConstant.USER_HEADER_JWT) + )) + .addSecurityItem( + new SecurityRequirement() + .addList("bearer-jwt", Arrays.asList("read", "write")) + .addList("user-jwt", Arrays.asList("read", "write")) + ) + .info(new Info() + .title(applicationProperties.getName()) + .version(applicationProperties.getVersion()) + .description(applicationProperties.getDescription()) + .termsOfService("https://nav.no") + .contact(new Contact() + .url("https://nav-it.slack.com/archives/CA3P9NGA2") + .email("dolly@nav.no") + .name("Team Dolly") + ) + .license(new License() + .name("MIT License") + .url("https://opensource.org/licenses/MIT") + ) + ); + } + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/swagger").setViewName("redirect:/swagger-ui.html"); + } +} \ No newline at end of file diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/SecurityConfig.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/SecurityConfig.java new file mode 100644 index 00000000000..70c7746038a --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/SecurityConfig.java @@ -0,0 +1,38 @@ +package no.nav.testnav.dollysearchservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; + +@EnableWebSecurity +@Configuration +@Profile({ "prod", "local" }) +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + + httpSecurity.sessionManagement(sessionConfig -> sessionConfig.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorizeConfig -> authorizeConfig.requestMatchers( + "/internal/**", + "/webjars/**", + "/swagger-resources/**", + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger", + "/error", + "/swagger-ui.html" + ).permitAll().requestMatchers("/api/**").fullyAuthenticated()) + .oauth2ResourceServer(oauth2RSConfig -> oauth2RSConfig.jwt(Customizer.withDefaults())); + + return httpSecurity.build(); + } +} + diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/credentials/ElasticSearchCredentials.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/credentials/ElasticSearchCredentials.java new file mode 100644 index 00000000000..142b0f3f58a --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/config/credentials/ElasticSearchCredentials.java @@ -0,0 +1,18 @@ +package no.nav.testnav.dollysearchservice.config.credentials; + + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "elasticsearch.client") +public class ElasticSearchCredentials { + private String host; + private String port; + private String username; + private String password; +} diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/consumer/OpenSearchConsumer.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/consumer/OpenSearchConsumer.java new file mode 100644 index 00000000000..3c29c69e86a --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/consumer/OpenSearchConsumer.java @@ -0,0 +1,52 @@ +package no.nav.testnav.dollysearchservice.consumer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import no.nav.testnav.dollysearchservice.config.Consumers; +import no.nav.testnav.dollysearchservice.consumer.command.OpenSearchCommand; +import no.nav.testnav.dollysearchservice.dto.SearchRequest; +import no.nav.testnav.dollysearchservice.dto.SearchResponse; +import no.nav.testnav.libs.securitycore.domain.ServerProperties; +import no.nav.testnav.libs.servletsecurity.exchange.TokenExchange; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; + +import static no.nav.testnav.dollysearchservice.consumer.utils.JacksonExchangeStrategyUtil.getJacksonStrategy; + +@Slf4j +@Component +public class OpenSearchConsumer { + + private final WebClient webClient; + private final TokenExchange tokenExchange; + private final ServerProperties serverProperties; + + public OpenSearchConsumer( + TokenExchange tokenExchange, + Consumers consumers, + ObjectMapper objectMapper, + WebClient.Builder webClientBuilder + ) { + + serverProperties = consumers.getTestnavPdlProxy(); + this.webClient = webClientBuilder + .baseUrl(serverProperties.getUrl()) + .exchangeStrategies(getJacksonStrategy(objectMapper)) + .build(); + this.tokenExchange = tokenExchange; + } + + @SneakyThrows + public Flux search(SearchRequest request) { + return tokenExchange.exchange(serverProperties) + .flatMapMany(token -> + new OpenSearchCommand(webClient, request.getQuery().indices()[0], + token.getTokenValue(), request.getQuery().source().toString()).call()) + .map(response -> { + response.setRequest(request.getRequest()); + return response; + }); + } +} \ No newline at end of file diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/consumer/command/OpenSearchCommand.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/consumer/command/OpenSearchCommand.java new file mode 100644 index 00000000000..9fcaa76c525 --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/consumer/command/OpenSearchCommand.java @@ -0,0 +1,48 @@ +package no.nav.testnav.dollysearchservice.consumer.command; + +import lombok.RequiredArgsConstructor; +import no.nav.testnav.dollysearchservice.dto.SearchResponse; +import no.nav.testnav.libs.reactivecore.utils.WebClientFilter; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +import java.time.Duration; +import java.util.concurrent.Callable; + +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.apache.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + + +@RequiredArgsConstructor +public class OpenSearchCommand implements Callable> { + + private static final String SEARCH_URL = "/pdl-elastic/{index}/_search"; + + private final WebClient webClient; + private final String index; + private final String token; + private final Object body; + + @Override + public Mono call() { + + return webClient.post() + .uri(uriBuilder -> uriBuilder + .path(SEARCH_URL) + .build(index)) + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .header(AUTHORIZATION, "Bearer " + token) + .body(BodyInserters.fromValue(body)) + .retrieve() + .bodyToMono(SearchResponse.class) + .retryWhen(Retry.backoff(3, Duration.ofSeconds(5)) + .filter(WebClientFilter::is5xxException)) + .onErrorResume(throwable -> Mono.just(SearchResponse.builder() + .status(WebClientFilter.getStatus(throwable)) + .error(WebClientFilter.getMessage(throwable)) + .build())); + } +} diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/consumer/utils/JacksonExchangeStrategyUtil.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/consumer/utils/JacksonExchangeStrategyUtil.java new file mode 100644 index 00000000000..5d039494015 --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/consumer/utils/JacksonExchangeStrategyUtil.java @@ -0,0 +1,25 @@ +package no.nav.testnav.dollysearchservice.consumer.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.experimental.UtilityClass; +import org.springframework.http.MediaType; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.web.reactive.function.client.ExchangeStrategies; + +@UtilityClass +public final class JacksonExchangeStrategyUtil { + + public static ExchangeStrategies getJacksonStrategy(ObjectMapper objectMapper) { + return ExchangeStrategies.builder() + .codecs(config -> { + config.defaultCodecs() + .maxInMemorySize(32 * 1024 * 1024); + config.defaultCodecs() + .jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON)); + config.defaultCodecs() + .jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON)); + }) + .build(); + } +} diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/dto/SearchRequest.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/dto/SearchRequest.java new file mode 100644 index 00000000000..a1c47ee3f83 --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/dto/SearchRequest.java @@ -0,0 +1,16 @@ +package no.nav.testnav.dollysearchservice.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SearchRequest { + + private no.nav.testnav.libs.data.dollysearchservice.v1.SearchRequest request; + private org.opensearch.action.search.SearchRequest query; +} diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/dto/SearchResponse.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/dto/SearchResponse.java new file mode 100644 index 00000000000..7800ad9bdc7 --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/dto/SearchResponse.java @@ -0,0 +1,58 @@ +package no.nav.testnav.dollysearchservice.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import no.nav.testnav.libs.data.dollysearchservice.v1.SearchRequest; +import org.springframework.http.HttpStatus; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SearchResponse { + + private SearchRequest request; + + private Integer took; + private Boolean timedOut; + private SearchHits hits; + + private HttpStatus status; + private String error; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class SearchHits { + + private Total total; + private float maxScore; + private List hits; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Total { + + private Long value; + private String relation; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @SuppressWarnings("java:S116") + public static class SearchHit { + + private String _index; + private String _type; + private String _id; + private Double _score; + private Object _source; + } +} diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/provider/OpensearchController.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/provider/OpensearchController.java new file mode 100644 index 00000000000..f704bc86dbd --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/provider/OpensearchController.java @@ -0,0 +1,29 @@ +package no.nav.testnav.dollysearchservice.provider; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.testnav.dollysearchservice.service.OpenSearchService; +import no.nav.testnav.libs.data.dollysearchservice.v1.SearchRequest; +import no.nav.testnav.libs.data.dollysearchservice.v1.SearchResponse; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@Slf4j +@RestController +@RequestMapping("/api/v1/opensearch") +@RequiredArgsConstructor +public class OpensearchController { + + private final OpenSearchService openSearchService; + + @PostMapping + @Operation(description = "Henter personer som matcher søk av persondetaljer i request") + public Mono getPersoner(@RequestBody SearchRequest request) { + + return openSearchService.search(request); + } +} diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/service/OpenSearchService.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/service/OpenSearchService.java new file mode 100644 index 00000000000..13cc7855d75 --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/service/OpenSearchService.java @@ -0,0 +1,81 @@ +package no.nav.testnav.dollysearchservice.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.testnav.dollysearchservice.consumer.OpenSearchConsumer; +import no.nav.testnav.dollysearchservice.utils.OpenSearchQueryBuilder; +import no.nav.testnav.libs.data.dollysearchservice.v1.SearchRequest; +import no.nav.testnav.libs.data.dollysearchservice.v1.SearchResponse; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.util.concurrent.TimeUnit; + +import static java.util.Objects.isNull; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@Slf4j +@Service +@RequiredArgsConstructor +public class OpenSearchService { + + private final OpenSearchConsumer openSearchConsumer; + private final ObjectMapper objectMapper; + + public Mono search(SearchRequest request) { + + var query = OpenSearchQueryBuilder.buildSearchQuery(request); + return execQuery(request, query); + } + + private Mono execQuery(SearchRequest request, BoolQueryBuilder query) { + + if (isNull(request.getSide())) { + request.setSide(1); + } + + if (isNull(request.getAntall())) { + request.setAntall(10); + } + + return Mono.from(openSearchConsumer.search( + no.nav.testnav.dollysearchservice.dto.SearchRequest.builder() + .query( + new org.opensearch.action.search.SearchRequest() + .indices("pdl-sok") + .source(new SearchSourceBuilder() + .query(query) + .from(request.getSide() * request.getAntall()) + .size(request.getAntall()) + .timeout(new TimeValue(3, TimeUnit.SECONDS)))) + .request(request) + .build())) + .map(this::formatResponse); + } + + private SearchResponse formatResponse(no.nav.testnav.dollysearchservice.dto.SearchResponse response) { + + if (isNotBlank(response.getError())) { + return SearchResponse.builder() + .error(response.getError()) + .build(); + } + + return SearchResponse.builder() + .took(response.getTook().toString()) + .totalHits(response.getHits().getTotal().getValue()) + .antall(response.getHits().getHits().size()) + .side(response.getRequest().getSide()) + .seed(response.getRequest().getSeed()) + .personer(response.getHits().getHits().stream() + .map(no.nav.testnav.dollysearchservice.dto.SearchResponse.SearchHit::get_source) + .map(person -> objectMapper.convertValue(person, JsonNode.class)) + .toList()) + .build(); + } +} diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/utils/OpenSearchIdenterQueryUtils.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/utils/OpenSearchIdenterQueryUtils.java new file mode 100644 index 00000000000..3a1ad74ce2a --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/utils/OpenSearchIdenterQueryUtils.java @@ -0,0 +1,52 @@ +package no.nav.testnav.dollysearchservice.utils; + +import lombok.experimental.UtilityClass; +import no.nav.testnav.libs.data.dollysearchservice.v1.SearchRequest; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.QueryBuilders; + +import java.util.Set; + +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.FOLKEREGISTERIDENTIFIKATOR; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.HENT_IDENTER; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.METADATA_HISTORISK; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.NAVSPERSONIDENTIFIKATOR; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.matchQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.nestedMatchQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.nestedRegexpQuery; + +@UtilityClass +public class OpenSearchIdenterQueryUtils { + + public static BoolQueryBuilder addIdenterIdentifier(SearchRequest request) { + + return request.getIdenter().isEmpty() ? + + QueryBuilders.boolQuery() + .must(addDollyIdentifier()) : + + QueryBuilders.boolQuery() + .must(addIdenterQuery(request.getIdenter())); + } + + private static BoolQueryBuilder addDollyIdentifier() { + + return QueryBuilders.boolQuery() + .should(matchQuery("tags", "DOLLY")) + .should(QueryBuilders.boolQuery() + .must(nestedMatchQuery(FOLKEREGISTERIDENTIFIKATOR, METADATA_HISTORISK, false)) + .must(nestedRegexpQuery(FOLKEREGISTERIDENTIFIKATOR, "identifikasjonsnummer", "\\d{2}[4-5]\\d{8}"))) + .should(QueryBuilders.boolQuery() + .must(nestedMatchQuery(NAVSPERSONIDENTIFIKATOR, METADATA_HISTORISK, false)) + .must(nestedRegexpQuery(NAVSPERSONIDENTIFIKATOR, "identifikasjonsnummer", "\\d{2}[6-7]\\d{8}"))); + } + + private static BoolQueryBuilder addIdenterQuery(Set identer) { + + var query = QueryBuilders.boolQuery(); + identer.forEach(ident -> query + .should(nestedMatchQuery(HENT_IDENTER, "ident", ident)) + ); + return query; + } +} diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/utils/OpenSearchPersonQueryUtils.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/utils/OpenSearchPersonQueryUtils.java new file mode 100644 index 00000000000..8036c6cbf60 --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/utils/OpenSearchPersonQueryUtils.java @@ -0,0 +1,399 @@ +package no.nav.testnav.dollysearchservice.utils; + +import lombok.experimental.UtilityClass; +import no.nav.testnav.libs.data.dollysearchservice.v1.SearchRequest; +import no.nav.testnav.libs.data.pdlforvalter.v1.Identtype; +import org.apache.lucene.search.join.ScoreMode; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.QueryBuilders; + +import java.time.LocalDate; +import java.util.Optional; + +import static java.util.Objects.nonNull; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.FOLKEREGISTERIDENTIFIKATOR; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.HENT_IDENTER; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.HISTORISK; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.METADATA_HISTORISK; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.NAVSPERSONIDENTIFIKATOR; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.nestedExistQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.nestedMatchQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchQueryUtils.rangeQuery; +import static org.apache.commons.lang3.BooleanUtils.isTrue; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@UtilityClass +public class OpenSearchPersonQueryUtils { + + private static final String FAMILIE_RELASJON_PATH = "hentPerson.forelderBarnRelasjon"; + private static final String BOSTEDSADRESSE = "hentPerson.bostedsadresse"; + private static final String OPPHOLDSADRESSE = "hentPerson.oppholdsadresse"; + private static final String KONTAKTADRESSE = "hentPerson.kontaktadresse"; + private static final String VEGADRESSE = "vegadresse"; + private static final String MATRIKKELADRESSE = "matrikkeladresse"; + private static final String UTENLANDSKADRESSE = "utenlandskAdresse"; + private static final String KOMMUNENUMMER = "kommunenummer"; + private static final String POSTNUMMER = "postnummer"; + private static final String BYDELSNUMMER = "bydelsnummer"; + private static final String CONCAT = "%s.%s"; + + public static void addAlderQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + var thisYear = LocalDate.now().getYear(); + if (nonNull(request.getPersonRequest().getAlderFom()) || nonNull(request.getPersonRequest().getAlderTom())) { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery("hentPerson.foedselsdato", METADATA_HISTORISK, false)) + .must(QueryBuilders.nestedQuery("hentPerson.foedselsdato", + QueryBuilders.boolQuery().must( + rangeQuery("hentPerson.foedselsdato.foedselsaar", + Optional.ofNullable(request.getPersonRequest().getAlderTom()) + .map(alderTom -> thisYear - alderTom) + .orElse(null), + Optional.ofNullable(request.getPersonRequest().getAlderFom()) + .map(alderFom -> thisYear - alderFom) + .orElse(null))), ScoreMode.Avg))); + } + } + + public static void addHarBarnQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarBarn())) { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery(FAMILIE_RELASJON_PATH, METADATA_HISTORISK, false)) + .must(nestedMatchQuery(FAMILIE_RELASJON_PATH, "relatertPersonsRolle", "BARN")) + ); + } + } + + public static void addHarForeldreQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarForeldre())) { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery(FAMILIE_RELASJON_PATH, METADATA_HISTORISK, false)) + .must(nestedMatchQuery(FAMILIE_RELASJON_PATH, "minRolleForPerson", "BARN")) + ); + } + } + + public static void addSivilstandQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (nonNull(request.getPersonRequest().getSivilstand())) { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery("hentPerson.sivilstand", METADATA_HISTORISK, false)) + .must(nestedMatchQuery("hentPerson.sivilstand", "type", + request.getPersonRequest().getSivilstand().name()) + )); + } + } + + public static void addHarDoedfoedtbarnQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarDoedfoedtBarn())) { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery("hentPerson.doedfoedtBarn", METADATA_HISTORISK, false))); + } + } + + public static void addHarForeldreansvarQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarForeldreAnsvar())) { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery("hentPerson.foreldreansvar", METADATA_HISTORISK, false))); + } + } + + public static void addVergemaalQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarVerge())) { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery("hentPerson.vergemaalEllerFremtidsfullmakt", METADATA_HISTORISK, false))); + } + } + + public static void addDoedsfallQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarDoedsfall())) { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery("hentPerson.doedsfall", METADATA_HISTORISK, false))); + } + } + + public static void addHarInnflyttingQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarInnflytting())) { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery("hentPerson.innflyttingTilNorge", METADATA_HISTORISK, false))); + } + } + + public static void addHarUtflyttingQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarUtflytting())) { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery("hentPerson.utflyttingFraNorge", METADATA_HISTORISK, false))); + } + } + + public static void addAdressebeskyttelseQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + Optional.ofNullable(request.getPersonRequest().getAdresse()) + .filter(adresse -> nonNull(adresse.getAddressebeskyttelse())) + .ifPresent(adresse -> + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery("hentPerson.adressebeskyttelse", METADATA_HISTORISK, false)) + .must(nestedMatchQuery("hentPerson.adressebeskyttelse", "gradering", + adresse.getAddressebeskyttelse().name())) + )); + } + + public static void addHarBostedsadresseQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + Optional.ofNullable(request.getPersonRequest().getAdresse()) + .filter(adresse -> isTrue(adresse.getHarBostedsadresse())) + .ifPresent(adresse -> + queryBuilder.must(nestedMatchQuery(BOSTEDSADRESSE, METADATA_HISTORISK, false)) + ); + } + + public static void addHarOppholdsadresseQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + Optional.ofNullable(request.getPersonRequest().getAdresse()) + .filter(adresse -> isTrue(adresse.getHarOppholdsadresse())) + .ifPresent(adresse -> + queryBuilder.must(nestedMatchQuery(OPPHOLDSADRESSE, METADATA_HISTORISK, false)) + ); + } + + public static void addHarKontaktadresseQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + Optional.ofNullable(request.getPersonRequest().getAdresse()) + .filter(adresse -> isTrue(adresse.getHarKontaktadresse())) + .ifPresent(adresse -> + queryBuilder.must(nestedMatchQuery(KONTAKTADRESSE, METADATA_HISTORISK, false)) + ); + } + + public static BoolQueryBuilder addAdresseQuery(String field, String value) { + + return QueryBuilders.boolQuery() + .should(QueryBuilders.boolQuery() + .must(nestedMatchQuery(BOSTEDSADRESSE, METADATA_HISTORISK, false)) + .must(QueryBuilders.boolQuery() + .should(nestedMatchQuery(BOSTEDSADRESSE, CONCAT.formatted(VEGADRESSE, field), value)) + .should(nestedMatchQuery(BOSTEDSADRESSE, CONCAT.formatted(MATRIKKELADRESSE, field), value)) + )) + .should(QueryBuilders.boolQuery() + .must(nestedMatchQuery(OPPHOLDSADRESSE, METADATA_HISTORISK, false)) + .must(QueryBuilders.boolQuery() + .should(nestedMatchQuery(OPPHOLDSADRESSE, CONCAT.formatted(VEGADRESSE, field), value)) + .should(nestedMatchQuery(OPPHOLDSADRESSE, CONCAT.formatted(MATRIKKELADRESSE, field), value)) + )) + .should(QueryBuilders.boolQuery() + .must(nestedMatchQuery(KONTAKTADRESSE, METADATA_HISTORISK, false)) + .must(nestedMatchQuery(KONTAKTADRESSE, CONCAT.formatted(VEGADRESSE, field), value)) + ); + } + + public static void addAdresseKommunenrQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + Optional.ofNullable(request.getPersonRequest().getAdresse()) + .filter(adresse -> isNotBlank(adresse.getKommunenummer())) + .ifPresent(adresse -> + queryBuilder.must(QueryBuilders.boolQuery() + .should(addAdresseQuery(KOMMUNENUMMER, adresse.getKommunenummer())) + .should(QueryBuilders.boolQuery() + .must(nestedMatchQuery(BOSTEDSADRESSE, METADATA_HISTORISK, false)) + .must(nestedMatchQuery(BOSTEDSADRESSE, "ukjentBosted.bostedskommune", adresse.getKommunenummer())) + ))); + } + + public static void addAdressePostnrQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + Optional.ofNullable(request.getPersonRequest().getAdresse()) + .filter(adresse -> isNotBlank(adresse.getPostnummer())) + .ifPresent(adresse -> + queryBuilder.must(QueryBuilders.boolQuery() + .should(addAdresseQuery(POSTNUMMER, adresse.getPostnummer())) + .should(QueryBuilders.boolQuery() + .must(nestedMatchQuery(KONTAKTADRESSE, METADATA_HISTORISK, false)) + .must(nestedMatchQuery(KONTAKTADRESSE, "postboksadresse." + POSTNUMMER, adresse.getPostnummer())) + ))); + } + + public static void addAdresseBydelsnrQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + Optional.ofNullable(request.getPersonRequest().getAdresse()) + .filter(adresse -> isNotBlank(adresse.getBydelsnummer())) + .ifPresent(adresse -> + queryBuilder.must(addAdresseQuery(BYDELSNUMMER, adresse.getBydelsnummer()) + )); + } + + public static void addHarAdresseBydelsnummerQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + Optional.ofNullable(request.getPersonRequest().getAdresse()) + .filter(adresse -> isTrue(adresse.getHarBydelsnummer())) + .ifPresent(adresse -> + queryBuilder.must(QueryBuilders.boolQuery() + .should(QueryBuilders.boolQuery() + .must(nestedMatchQuery(BOSTEDSADRESSE, METADATA_HISTORISK, false)) + .must(QueryBuilders.boolQuery() + .should(nestedExistQuery(BOSTEDSADRESSE, CONCAT.formatted(VEGADRESSE, BYDELSNUMMER))) + .should(nestedExistQuery(BOSTEDSADRESSE, CONCAT.formatted(MATRIKKELADRESSE, BYDELSNUMMER))))) + .should(QueryBuilders.boolQuery() + .must(nestedMatchQuery(OPPHOLDSADRESSE, METADATA_HISTORISK, false)) + .must(QueryBuilders.boolQuery() + .should(nestedExistQuery(OPPHOLDSADRESSE, CONCAT.formatted(VEGADRESSE, BYDELSNUMMER))) + .should(nestedExistQuery(OPPHOLDSADRESSE, CONCAT.formatted(MATRIKKELADRESSE, BYDELSNUMMER))))) + .should(QueryBuilders.boolQuery() + .must(nestedMatchQuery(KONTAKTADRESSE, METADATA_HISTORISK, false)) + .must(nestedExistQuery(KONTAKTADRESSE, CONCAT.formatted(VEGADRESSE, BYDELSNUMMER)))) + )); + } + + public static void addAdresseUtlandQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + Optional.ofNullable(request.getPersonRequest().getAdresse()) + .filter(boadresse -> isTrue(boadresse.getHarUtenlandsadresse())) + .ifPresent(boadresse -> + queryBuilder.must(QueryBuilders.boolQuery() + .should(QueryBuilders.boolQuery() + .must(nestedExistQuery(BOSTEDSADRESSE, UTENLANDSKADRESSE)) + .must(nestedMatchQuery(BOSTEDSADRESSE, METADATA_HISTORISK, false))) + .should(QueryBuilders.boolQuery() + .must(nestedExistQuery(OPPHOLDSADRESSE, UTENLANDSKADRESSE)) + .must(nestedMatchQuery(OPPHOLDSADRESSE, METADATA_HISTORISK, false))) + .should(QueryBuilders.boolQuery() + .must(nestedExistQuery(KONTAKTADRESSE, UTENLANDSKADRESSE)) + .must(nestedMatchQuery(KONTAKTADRESSE, METADATA_HISTORISK, false)) + ))); + } + + public static void addAdresseMatrikkelQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + Optional.ofNullable(request.getPersonRequest().getAdresse()) + .filter(boadresse -> isTrue(boadresse.getHarMatrikkeladresse())) + .ifPresent(boadresse -> + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery(BOSTEDSADRESSE, METADATA_HISTORISK, false)) + .must(nestedExistQuery(BOSTEDSADRESSE, MATRIKKELADRESSE)) + )); + } + + public static void addHarBostedUkjentQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + Optional.ofNullable(request.getPersonRequest().getAdresse()) + .filter(boadresse -> isTrue(boadresse.getHarUkjentAdresse())) + .ifPresent(boadresse -> + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery(BOSTEDSADRESSE, METADATA_HISTORISK, false)) + .must(nestedExistQuery(BOSTEDSADRESSE, "ukjentBosted")) + )); + } + + public static void addHarDeltBostedQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + Optional.ofNullable(request.getPersonRequest().getAdresse()) + .filter(adresse -> isTrue(adresse.getHarDeltBosted())) + .ifPresent(adresse -> + queryBuilder.must(nestedMatchQuery("hentPerson.deltBosted", METADATA_HISTORISK, false)) + ); + } + + public static void addHarKontaktinformasjonForDoedsboQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarKontaktinformasjonForDoedsbo())) { + queryBuilder.must(nestedMatchQuery("hentPerson.kontaktinformasjonForDoedsbo", METADATA_HISTORISK, false)); + } + } + + public static void addHarUtenlandskIdentifikasjonsnummerQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarUtenlandskIdentifikasjonsnummer())) { + queryBuilder.must(nestedMatchQuery("hentPerson.utenlandskIdentifikasjonsnummer", METADATA_HISTORISK, false)); + } + } + + public static void addHarFalskIdentitetQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarFalskIdentitet())) { + queryBuilder.must(nestedMatchQuery("hentPerson.falskIdentitet", METADATA_HISTORISK, false)); + } + } + + public static void addHarTilrettelagtKommunikasjonQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarTilrettelagtKommunikasjon())) { + queryBuilder.must(nestedMatchQuery("hentPerson.tilrettelagtKommunikasjon", METADATA_HISTORISK, false)); + } + } + + public static void addHarSikkerhetstiltakQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarSikkerhetstiltak())) { + queryBuilder.must(nestedMatchQuery("hentPerson.sikkerhetstiltak", METADATA_HISTORISK, false)); + } + } + + public static void addStatsborgerskapQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isNotBlank(request.getPersonRequest().getStatsborgerskap())) { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery("hentPerson.statsborgerskap", METADATA_HISTORISK, false)) + .must(nestedMatchQuery("hentPerson.statsborgerskap", "land", + request.getPersonRequest().getStatsborgerskap())) + ); + } + } + + public static void addHarOppholdQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarOpphold())) { + queryBuilder.must(nestedMatchQuery("hentPerson.opphold", METADATA_HISTORISK, false)); + } + } + + public static void addHarNyIdentitetQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (isTrue(request.getPersonRequest().getHarNyIdentitet())) { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery(HENT_IDENTER, HISTORISK, false)) + .must(nestedMatchQuery(HENT_IDENTER, HISTORISK, true)) + ); + } + } + + public static void addKjoennQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (nonNull(request.getPersonRequest().getKjoenn())) { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery("hentPerson.kjoenn", METADATA_HISTORISK, false)) + .must(nestedMatchQuery("hentPerson.kjoenn", "kjoenn", + request.getPersonRequest().getKjoenn().name())) + ); + } + } + + public static void addIdenttypeQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + if (nonNull(request.getPersonRequest().getIdenttype())) { + if (request.getPersonRequest().getIdenttype() == Identtype.NPID) { + queryBuilder.must(QueryBuilders.boolQuery() + .should(QueryBuilders.boolQuery() + .must(nestedMatchQuery(NAVSPERSONIDENTIFIKATOR, METADATA_HISTORISK, false)) + .must(nestedExistQuery(NAVSPERSONIDENTIFIKATOR, "identifikasjonsnummer")) + ) + .should(QueryBuilders.boolQuery() + .must(nestedMatchQuery(HENT_IDENTER, HISTORISK, false)) + .must(nestedMatchQuery(HENT_IDENTER, "gruppe", "NPID")) + ) + ); + } else { + queryBuilder.must(QueryBuilders.boolQuery() + .must(nestedMatchQuery(FOLKEREGISTERIDENTIFIKATOR, METADATA_HISTORISK, false)) + .must(nestedMatchQuery(FOLKEREGISTERIDENTIFIKATOR, "type", + request.getPersonRequest().getIdenttype().name()))); + } + } + } +} diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/utils/OpenSearchQueryBuilder.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/utils/OpenSearchQueryBuilder.java new file mode 100644 index 00000000000..55aafac8e22 --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/utils/OpenSearchQueryBuilder.java @@ -0,0 +1,115 @@ +package no.nav.testnav.dollysearchservice.utils; + +import lombok.experimental.UtilityClass; +import no.nav.testnav.libs.data.dollysearchservice.v1.SearchRequest; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.index.query.functionscore.FunctionScoreQueryBuilder; +import org.opensearch.index.query.functionscore.RandomScoreFunctionBuilder; + +import java.security.SecureRandom; +import java.util.Optional; +import java.util.Random; + +import static java.util.Objects.isNull; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchIdenterQueryUtils.addIdenterIdentifier; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addAdresseBydelsnrQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addAdresseKommunenrQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addAdresseMatrikkelQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addAdressePostnrQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addAdresseUtlandQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addAdressebeskyttelseQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addAlderQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addDoedsfallQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarAdresseBydelsnummerQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarBarnQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarBostedUkjentQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarBostedsadresseQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarDeltBostedQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarDoedfoedtbarnQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarFalskIdentitetQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarForeldreQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarForeldreansvarQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarInnflyttingQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarKontaktadresseQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarKontaktinformasjonForDoedsboQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarNyIdentitetQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarOppholdQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarOppholdsadresseQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarSikkerhetstiltakQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarTilrettelagtKommunikasjonQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarUtenlandskIdentifikasjonsnummerQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addHarUtflyttingQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addIdenttypeQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addKjoennQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addSivilstandQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addStatsborgerskapQuery; +import static no.nav.testnav.dollysearchservice.utils.OpenSearchPersonQueryUtils.addVergemaalQuery; + +@UtilityClass +public class OpenSearchQueryBuilder { + + private static final Random SEED = new SecureRandom(); + + public static BoolQueryBuilder buildSearchQuery(SearchRequest request) { + + var queryBuilder = QueryBuilders.boolQuery() + .must(getRandomScoreQueryBuilder(request)) + .must(addIdenterIdentifier(request)); + + setPersonQuery(queryBuilder, request); + + return queryBuilder; + } + + private static void setPersonQuery(BoolQueryBuilder queryBuilder, SearchRequest request) { + + Optional.ofNullable(request.getPersonRequest()) + .ifPresent(value -> { + + addAlderQuery(queryBuilder, request); + addHarBarnQuery(queryBuilder, request); + addHarForeldreQuery(queryBuilder, request); + addSivilstandQuery(queryBuilder, request); + addHarDoedfoedtbarnQuery(queryBuilder, request); + addHarForeldreansvarQuery(queryBuilder, request); + addVergemaalQuery(queryBuilder, request); + addDoedsfallQuery(queryBuilder, request); + addHarInnflyttingQuery(queryBuilder, request); + addHarUtflyttingQuery(queryBuilder, request); + addAdressebeskyttelseQuery(queryBuilder, request); + addHarBostedsadresseQuery(queryBuilder, request); + addHarOppholdsadresseQuery(queryBuilder, request); + addHarKontaktadresseQuery(queryBuilder, request); + addAdressebeskyttelseQuery(queryBuilder, request); + addAdresseKommunenrQuery(queryBuilder, request); + addAdressePostnrQuery(queryBuilder, request); + addAdresseBydelsnrQuery(queryBuilder, request); + addHarAdresseBydelsnummerQuery(queryBuilder, request); + addAdresseUtlandQuery(queryBuilder, request); + addAdresseMatrikkelQuery(queryBuilder, request); + addHarBostedUkjentQuery(queryBuilder, request); + addHarDeltBostedQuery(queryBuilder, request); + addHarKontaktinformasjonForDoedsboQuery(queryBuilder, request); + addHarUtenlandskIdentifikasjonsnummerQuery(queryBuilder, request); + addHarFalskIdentitetQuery(queryBuilder, request); + addHarTilrettelagtKommunikasjonQuery(queryBuilder, request); + addHarSikkerhetstiltakQuery(queryBuilder, request); + addStatsborgerskapQuery(queryBuilder, request); + addHarOppholdQuery(queryBuilder, request); + addHarNyIdentitetQuery(queryBuilder, request); + addKjoennQuery(queryBuilder, request); + addIdenttypeQuery(queryBuilder, request); + }); + } + + private static FunctionScoreQueryBuilder getRandomScoreQueryBuilder(SearchRequest request) { + + if (isNull(request.getSeed())){ + request.setSeed(SEED.nextInt()); + } + + return QueryBuilders.functionScoreQuery(new RandomScoreFunctionBuilder() + .seed(request.getSeed())); + } +} diff --git a/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/utils/OpenSearchQueryUtils.java b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/utils/OpenSearchQueryUtils.java new file mode 100644 index 00000000000..7cf100b78da --- /dev/null +++ b/apps/dolly-search-service/src/main/java/no/nav/testnav/dollysearchservice/utils/OpenSearchQueryUtils.java @@ -0,0 +1,51 @@ +package no.nav.testnav.dollysearchservice.utils; + +import lombok.experimental.UtilityClass; +import org.apache.lucene.search.join.ScoreMode; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.QueryBuilders; + +@UtilityClass +public class OpenSearchQueryUtils { + + public static final String HENT_IDENTER = "hentIdenter.identer"; + public static final String HISTORISK = "historisk"; + public static final String METADATA_HISTORISK = "metadata.historisk"; + public static final String FOLKEREGISTERIDENTIFIKATOR = "hentPerson.folkeregisteridentifikator"; + public static final String NAVSPERSONIDENTIFIKATOR = "hentPerson.navspersonidentifikator"; + + public static QueryBuilder rangeQuery(String field, Integer value1, Integer value2) { + + return QueryBuilders.rangeQuery(field).from(value1).to(value2); + } + + public static QueryBuilder matchQuery(String field, Object value) { + + return QueryBuilders.matchQuery(field, value); + } + + public static QueryBuilder existQuery(String field) { + + return QueryBuilders.existsQuery(field); + } + + public static QueryBuilder regexpQuery(String field, String value) { + + return QueryBuilders.regexpQuery(field, value); + } + + public static QueryBuilder nestedRegexpQuery(String path, String field, String value) { + + return QueryBuilders.nestedQuery(path, regexpQuery("%s.%s".formatted(path, field), value), ScoreMode.Avg); + } + + public static QueryBuilder nestedMatchQuery(String path, String field, Object value) { + + return QueryBuilders.nestedQuery(path, matchQuery("%s.%s".formatted(path, field), value), ScoreMode.Avg); + } + + public static QueryBuilder nestedExistQuery(String path, String field) { + + return QueryBuilders.nestedQuery(path, existQuery(path + '.' + field), ScoreMode.Avg); + } +} diff --git a/apps/dolly-search-service/src/main/resources/application-local.yml b/apps/dolly-search-service/src/main/resources/application-local.yml new file mode 100644 index 00000000000..f3ed84e8daf --- /dev/null +++ b/apps/dolly-search-service/src/main/resources/application-local.yml @@ -0,0 +1,13 @@ +AZURE_APP_CLIENT_ID: ${sm\://azure-app-client-id} +AZURE_APP_CLIENT_SECRET: ${sm\://azure-app-client-secret} +TOKEN_X_ISSUER: https://tokenx.dev-gcp.nav.cloud.nais.io + +spring: + config: + import: "sm://" + +consumers: + dolly-backend: + url: https://dolly-backend.intern.dev.nav.no + dolly-backend-dev: + url: https://dolly-backend-dev.intern.dev.nav.no \ No newline at end of file diff --git a/apps/dolly-search-service/src/main/resources/application-prod.yml b/apps/dolly-search-service/src/main/resources/application-prod.yml new file mode 100644 index 00000000000..37735b8c79c --- /dev/null +++ b/apps/dolly-search-service/src/main/resources/application-prod.yml @@ -0,0 +1,3 @@ +testnorge: + analyse: + enabled: true diff --git a/apps/dolly-search-service/src/main/resources/application.yml b/apps/dolly-search-service/src/main/resources/application.yml new file mode 100644 index 00000000000..a55c76a9699 --- /dev/null +++ b/apps/dolly-search-service/src/main/resources/application.yml @@ -0,0 +1,55 @@ +AAD_ISSUER_URI: https://login.microsoftonline.com/62366534-1ec3-4962-8869-9b5535279d0b + +spring: + application: + name: testnav-dolly-search-service + description: Tjeneste for å søke etter Dolly-personer. + security: + oauth2: + resourceserver: + aad: + issuer-uri: ${AAD_ISSUER_URI}/v2.0 + jwk-set-uri: ${AAD_ISSUER_URI}/discovery/v2.0/keys + accepted-audience: ${AZURE_APP_CLIENT_ID}, api:// ${AZURE_APP_CLIENT_ID} + tokenx: + issuer-uri: ${TOKEN_X_ISSUER} + jwk-set-uri: ${TOKEN_X_JWKS_URI} + accepted-audience: ${TOKEN_X_CLIENT_ID} + +springdoc: + swagger-ui: + disable-swagger-default-url: true + url: /v3/api-docs + +consumers: + testnav-pdl-proxy: + name: testnav-pdl-proxy + namespace: dolly + url: https://testnav-pdl-proxy.dev-fss-pub.nais.io + cluster: dev-fss + +management: + health: + elasticsearch: + enabled: false + endpoints: + enabled-by-default: true + web: + base-path: /internal + exposure: + include: prometheus,health + path-mapping: + prometheus: metrics + endpoint: + prometheus: + enabled: true + prometheus: + metrics: + export: + enabled: true +server: + servlet: + encoding: + charset: UTF-8 + error: + include-message: always \ No newline at end of file diff --git a/apps/dolly-search-service/src/main/resources/logback-spring.xml b/apps/dolly-search-service/src/main/resources/logback-spring.xml new file mode 100644 index 00000000000..7f81f72aa83 --- /dev/null +++ b/apps/dolly-search-service/src/main/resources/logback-spring.xml @@ -0,0 +1,41 @@ + + + + + + + true + + 10280 + 20 + ^sun\.reflect\..*\.invoke + ^net\.sf\.cglib\.proxy\.MethodProxy\.invoke + java\.util\.concurrent\..* + org\.apache\.catalina\..* + org\.apache\.coyote\..* + org\.apache\.tomcat\..* + + + + + + + + + + + + + %d{HH:mm:ss.SSS} | %5p | %logger{25} | %m%n + + utf8 + + + + + + + + + \ No newline at end of file diff --git a/apps/dolly-search-service/src/test/java/no/nav/testnav/dollysearchservice/ApplicationContextTest.java b/apps/dolly-search-service/src/test/java/no/nav/testnav/dollysearchservice/ApplicationContextTest.java new file mode 100644 index 00000000000..b8ab4cf8efc --- /dev/null +++ b/apps/dolly-search-service/src/test/java/no/nav/testnav/dollysearchservice/ApplicationContextTest.java @@ -0,0 +1,22 @@ +package no.nav.testnav.dollysearchservice; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +@Disabled +@SpringBootTest +@ActiveProfiles("test") +class ApplicationContextTest { + + @MockitoBean + public JwtDecoder jwtDecoder; + + @Test + @SuppressWarnings("java:S2699") + void load_app_context() { + } +} \ No newline at end of file diff --git a/apps/dolly-search-service/src/test/resources/application-test.yml b/apps/dolly-search-service/src/test/resources/application-test.yml new file mode 100644 index 00000000000..34d5811b26d --- /dev/null +++ b/apps/dolly-search-service/src/test/resources/application-test.yml @@ -0,0 +1 @@ +TOKEN_X_ISSUER: https://tokenx.dev-gcp.nav.cloud.nais.io diff --git a/libs/data-transfer-search-objects/build.gradle b/libs/data-transfer-search-objects/build.gradle index 0ed6e488199..0f4335af098 100644 --- a/libs/data-transfer-search-objects/build.gradle +++ b/libs/data-transfer-search-objects/build.gradle @@ -11,6 +11,7 @@ sonarqube { } dependencies { + implementation "no.nav.testnav.libs:data-transfer-objects" implementation 'com.fasterxml.jackson.core:jackson-annotations' implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' diff --git a/libs/data-transfer-search-objects/settings.gradle b/libs/data-transfer-search-objects/settings.gradle index a1b6cd81d41..c64e942de3d 100644 --- a/libs/data-transfer-search-objects/settings.gradle +++ b/libs/data-transfer-search-objects/settings.gradle @@ -5,6 +5,7 @@ plugins { rootProject.name = 'data-transfer-search-objects' includeBuild "../../plugins/java" +includeBuild '../../libs/data-transfer-objects' develocity { buildScan { diff --git a/libs/data-transfer-search-objects/src/main/java/no/nav/testnav/libs/data/dollysearchservice/v1/PersonRequest.java b/libs/data-transfer-search-objects/src/main/java/no/nav/testnav/libs/data/dollysearchservice/v1/PersonRequest.java new file mode 100644 index 00000000000..4e873f4d09f --- /dev/null +++ b/libs/data-transfer-search-objects/src/main/java/no/nav/testnav/libs/data/dollysearchservice/v1/PersonRequest.java @@ -0,0 +1,67 @@ +package no.nav.testnav.libs.data.dollysearchservice.v1; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import no.nav.testnav.libs.data.pdlforvalter.v1.AdressebeskyttelseDTO; +import no.nav.testnav.libs.data.pdlforvalter.v1.Identtype; +import no.nav.testnav.libs.data.pdlforvalter.v1.KjoennDTO; +import no.nav.testnav.libs.data.pdlforvalter.v1.SivilstandDTO; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor(force = true) +public class PersonRequest { + + private Identtype identtype; + private KjoennDTO.Kjoenn kjoenn; + private Integer alderFom; + private Integer alderTom; + private SivilstandDTO.Sivilstand sivilstand; + + private Boolean harBarn; + private Boolean harForeldre; + private Boolean harDoedfoedtBarn; + private Boolean harForeldreAnsvar; + private Boolean harVerge; + private Boolean harDoedsfall; + private Boolean harInnflytting; + private Boolean harUtflytting; + private Boolean harKontaktinformasjonForDoedsbo; + private Boolean harUtenlandskIdentifikasjonsnummer; + private Boolean harFalskIdentitet; + private Boolean harTilrettelagtKommunikasjon; + private Boolean harSikkerhetstiltak; + private Boolean harOpphold; + @Schema(description = "landkode") + private String statsborgerskap; + private Boolean harNyIdentitet; + + private AdresseRequest adresse; + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor(force = true) + public static class AdresseRequest { + + private AdressebeskyttelseDTO.AdresseBeskyttelse addressebeskyttelse; + + private String kommunenummer; + private String postnummer; + private String bydelsnummer; + + private Boolean harBydelsnummer; + private Boolean harUtenlandsadresse; + private Boolean harMatrikkeladresse; + private Boolean harUkjentAdresse; + private Boolean harDeltBosted; + + private Boolean harBostedsadresse; + private Boolean harKontaktadresse; + private Boolean harOppholdsadresse; + } +} \ No newline at end of file diff --git a/libs/data-transfer-search-objects/src/main/java/no/nav/testnav/libs/data/dollysearchservice/v1/SearchRequest.java b/libs/data-transfer-search-objects/src/main/java/no/nav/testnav/libs/data/dollysearchservice/v1/SearchRequest.java new file mode 100644 index 00000000000..53a13db963c --- /dev/null +++ b/libs/data-transfer-search-objects/src/main/java/no/nav/testnav/libs/data/dollysearchservice/v1/SearchRequest.java @@ -0,0 +1,40 @@ +package no.nav.testnav.libs.data.dollysearchservice.v1; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.HashSet; +import java.util.Set; + +import static java.util.Objects.isNull; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SearchRequest { + + @Schema(description = "Sidenummer") + private Integer side; + @Schema(description = "Antall resultater per side") + private Integer antall; + @Schema(description = "Seed for paginering") + private Integer seed; + + @Schema(description = "Persondetaljer") + private PersonRequest personRequest; + + @Schema(description = "Identer fra registre") + private Set identer; + + public Set getIdenter() { + + if (isNull(identer)) { + identer = new HashSet<>(); + } + return identer; + } +} diff --git a/libs/data-transfer-search-objects/src/main/java/no/nav/testnav/libs/data/dollysearchservice/v1/SearchResponse.java b/libs/data-transfer-search-objects/src/main/java/no/nav/testnav/libs/data/dollysearchservice/v1/SearchResponse.java new file mode 100644 index 00000000000..d43b4841b42 --- /dev/null +++ b/libs/data-transfer-search-objects/src/main/java/no/nav/testnav/libs/data/dollysearchservice/v1/SearchResponse.java @@ -0,0 +1,24 @@ +package no.nav.testnav.libs.data.dollysearchservice.v1; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SearchResponse { + + private Long totalHits; + private String took; + private Integer side; + private Integer antall; + private Integer seed; + private List personer; + private String error; +} \ No newline at end of file diff --git a/plugins/java/src/main/groovy/dolly-versions.gradle b/plugins/java/src/main/groovy/dolly-versions.gradle index 822c0841829..9979b378763 100644 --- a/plugins/java/src/main/groovy/dolly-versions.gradle +++ b/plugins/java/src/main/groovy/dolly-versions.gradle @@ -31,7 +31,7 @@ class DollyVersionCatalog { String logback = "8.0" String mq = "3.3.5" String okhttp = "4.12.0" - String opensearch = "1.6.0" + String opensearch = "1.6.1" String orika = "1.5.4" String reactorSpring = "1.0.1.RELEASE" String reactorTest = "3.7.1" diff --git a/proxies/pdl-proxy/config.yml b/proxies/pdl-proxy/config.yml index 64a77463673..31d0ff9ecd5 100644 --- a/proxies/pdl-proxy/config.yml +++ b/proxies/pdl-proxy/config.yml @@ -19,8 +19,11 @@ metadata: labels: team: dolly annotations: - nginx.ingress.kubernetes.io/proxy-read-timeout: "2400" - nginx.ingress.kubernetes.io/proxy-send-timeout: "2400" + nginx.ingress.kubernetes.io/proxy-read-timeout: "240" + nginx.ingress.kubernetes.io/proxy-send-timeout: "240" + nginx.ingress.kubernetes.io/proxy-body-size: "8m" + nginx.ingress.kubernetes.io/proxy-buffer-size: "8m" + nginx.ingress.kubernetes.io/client-body-buffer-size: "8m" spec: image: "{{image}}" port: 8080 @@ -65,6 +68,8 @@ spec: cluster: dev-gcp - application: testnav-levende-arbeidsforhold-ansettelse cluster: dev-gcp + - application: testnav-dolly-search-service + cluster: dev-gcp outbound: rules: - application: pdl-testdata diff --git a/proxies/pdl-proxy/src/main/java/no/nav/testnav/proxies/pdlproxy/PdlProxyApplicationStarter.java b/proxies/pdl-proxy/src/main/java/no/nav/testnav/proxies/pdlproxy/PdlProxyApplicationStarter.java index 550ac31e673..33a6b602ef0 100644 --- a/proxies/pdl-proxy/src/main/java/no/nav/testnav/proxies/pdlproxy/PdlProxyApplicationStarter.java +++ b/proxies/pdl-proxy/src/main/java/no/nav/testnav/proxies/pdlproxy/PdlProxyApplicationStarter.java @@ -75,5 +75,4 @@ private Function> createRoute(ServerProperties s .map(AccessToken::getTokenValue)); return createRoute(segment, host, filter); } - -} +} \ No newline at end of file