NomGraphClient.java

  1. package no.nav.data.team.resource;

  2. import com.fasterxml.jackson.databind.node.ArrayNode;
  3. import com.github.benmanes.caffeine.cache.Cache;
  4. import com.github.benmanes.caffeine.cache.Caffeine;
  5. import lombok.RequiredArgsConstructor;
  6. import lombok.SneakyThrows;
  7. import lombok.extern.slf4j.Slf4j;
  8. import no.nav.data.common.security.SecurityProperties;
  9. import no.nav.data.common.security.TokenProvider;
  10. import no.nav.data.common.utils.DateUtil;
  11. import no.nav.data.common.utils.JsonUtils;
  12. import no.nav.data.common.utils.MetricUtils;
  13. import no.nav.data.common.utils.StreamUtils;
  14. import no.nav.data.team.integration.process.GraphQLRequest;
  15. import no.nav.data.team.org.OrgUrlId;
  16. import no.nav.data.team.resource.domain.Resource;
  17. import no.nav.data.team.resource.dto.NomGraphQlResponse.MultiRessurs;
  18. import no.nav.data.team.resource.dto.NomGraphQlResponse.SingleOrg;
  19. import no.nav.data.team.resource.dto.NomGraphQlResponse.SingleRessurs;
  20. import no.nav.data.team.resource.dto.NomGraphQlResponse.SingleRessurs.DataWrapper;
  21. import no.nav.data.team.resource.dto.ResourceUnitsResponse;
  22. import no.nav.nom.graphql.model.*;
  23. import org.springframework.boot.web.client.RestTemplateBuilder;
  24. import org.springframework.http.HttpHeaders;
  25. import org.springframework.http.client.ClientHttpRequestInterceptor;
  26. import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
  27. import org.springframework.lang.Nullable;
  28. import org.springframework.stereotype.Service;
  29. import org.springframework.util.ReflectionUtils;
  30. import org.springframework.web.client.RestOperations;
  31. import org.springframework.web.client.RestTemplate;

  32. import java.lang.reflect.Method;
  33. import java.time.Duration;
  34. import java.time.LocalDate;
  35. import java.util.ArrayList;
  36. import java.util.Collection;
  37. import java.util.List;
  38. import java.util.Map;
  39. import java.util.Objects;
  40. import java.util.Optional;
  41. import java.util.UUID;
  42. import java.util.stream.Stream;

  43. import static java.util.Objects.requireNonNull;
  44. import static no.nav.data.common.utils.StreamUtils.distinctByKey;
  45. import static no.nav.data.common.web.TraceHeaderRequestInterceptor.correlationInterceptor;

  46. /**
  47.  * Cannot be used in dev atm, as teamkat runs as nav.no, and nom as trygdeetaten.no
  48.  */
  49. @Slf4j
  50. @Service
  51. @RequiredArgsConstructor
  52. public class NomGraphClient {

  53.     private RestTemplate restTemplate;
  54.     private final RestTemplateBuilder restTemplateBuilder;
  55.     private final SecurityProperties securityProperties;
  56.     private final TokenProvider tokenProvider;
  57.     private final NomGraphQLProperties properties;

  58.     private static final String getResourceQuery = StreamUtils.readCpFile("nom/graphql/queries/get_org_for_ident.graphql");
  59.     private static final String getOrgQuery = StreamUtils.readCpFile("nom/graphql/queries/get_org_with_organiseringer.graphql");
  60.     private static final String getLeaderMemberQuery = StreamUtils.readCpFile("nom/graphql/queries/get_personer_for_org.graphql");
  61.     private static final String scopeTemplate = "api://%s-gcp.nom.nom-api/.default";

  62.     private static final Cache<String, RessursDto> ressursCache = MetricUtils.register("nomRessursCache",
  63.             Caffeine.newBuilder().recordStats()
  64.                     .expireAfterWrite(Duration.ofMinutes(10))
  65.                     .maximumSize(1000).build());

  66.     private static final Cache<String, OrgEnhetDto> orgCache = MetricUtils.register("nomOrgCache",
  67.             Caffeine.newBuilder().recordStats()
  68.                     .expireAfterWrite(Duration.ofMinutes(10))
  69.                     .maximumSize(1000).build());

  70.     private static final Cache<String, List<String>> leaderCache = MetricUtils.register("nomLeaderCache",
  71.             Caffeine.newBuilder().recordStats()
  72.                     .expireAfterWrite(Duration.ofMinutes(10))
  73.                     .maximumSize(1000).build());

  74.     public Optional<RessursDto> getRessurs(String navIdent) {
  75.         return Optional.ofNullable(getRessurser(List.of(navIdent)).get(navIdent));
  76.     }

  77.     public Optional<OrgEnhetDto> getOrgEnhet(String orgUrl) {
  78.         var org = orgCache.get(orgUrl, key -> {
  79.             var orgUrlData = new OrgUrlId(orgUrl);
  80.             Map<String,Object> orgMap = Map.of("agressoId",orgUrlData.getAgressoId(), "orgNiv", orgUrlData.getOrgNiv());

  81.             var req = new GraphQLRequest(getOrgQuery, orgMap);

  82.             var res = template().postForEntity(properties.getUrl(), req, SingleOrg.class);
  83.             logErrors("getOrgWithOrganiseringer", res.getBody());
  84.             var orgEnhet = requireNonNull(res.getBody()).getData().getOrgEnhet();
  85.             if (orgEnhet != null) {
  86.                 orgEnhet.setOrganiseringer(distinctByKey(orgEnhet.getOrganiseringer(), o -> o.getOrgEnhet().getAgressoId()));
  87.             }
  88.             return orgEnhet;
  89.         });
  90.         return Optional.ofNullable(org);
  91.     }

  92.     public Optional<ResourceUnitsResponse> getUnits(String navIdent) {
  93.         return getRessurs(navIdent)
  94.                 .map(r -> ResourceUnitsResponse.from(r, getLeaderMembersActiveOnly(navIdent), this::getOrgEnhet));
  95.     }

  96.     private Map<String, RessursDto> getRessurser(List<String> navIdents) {
  97.         return ressursCache.getAll(navIdents, idents -> {
  98.             var req = new GraphQLRequest(getResourceQuery, Map.of("navIdenter", idents));
  99.             var res = template().postForEntity(properties.getUrl(), req, MultiRessurs.class);
  100.             logErrors("getDepartments", res.getBody());
  101.             return requireNonNull(res.getBody()).getData().getRessurserAsMap();
  102.         });
  103.     }

  104.     public List<String> getLeaderMembersActiveOnly(String navIdent) {
  105.         var nomClient = NomClient.getInstance();
  106.         return getLeaderMembers(navIdent).stream()
  107.                 .map(nomClient::getByNavIdent)
  108.                 .filter(Optional::isPresent).map(Optional::get)
  109.                 .filter(it -> !it.isInactive()).map(Resource::getNavIdent).toList();
  110.     }

  111.     public List<String> getLeaderMembers(String navIdent) {
  112.         return leaderCache.get(navIdent, ident -> {
  113.             var req = new GraphQLRequest(getLeaderMemberQuery, Map.of("navident", navIdent));
  114.             var res = template().postForEntity(properties.getUrl(), req, SingleRessurs.class);
  115.             logErrors("getLeaderMembers", res.getBody());
  116.             var orgenheter = Optional.ofNullable(res.getBody())
  117.                     .map(SingleRessurs::getData)
  118.                     .map(DataWrapper::getRessurs)
  119.                     .stream()
  120.                     .map(RessursDto::getLederFor)
  121.                     .flatMap(Collection::stream)
  122.                     .map(LederOrgEnhetDto::getOrgEnhet)
  123.                     .filter(org -> DateUtil.isNow(org.getGyldigFom(), org.getGyldigTom())).toList();

  124.             var directMembers = new ArrayList<String>();
  125.             for (var org : orgenheter) {
  126.                 var refId = org.getId();
  127.                 var ressurser = org.getKoblinger().stream().map(OrgEnhetsKoblingDto::getRessurs);
  128.                 var okRessurser = ressurser.filter(it -> !it.getNavident().equals(navIdent) && this.ressursHarEnRelevantOrgtilknytning(it, refId));
  129.                 directMembers.addAll(okRessurser.map(RessursDto::getNavident).filter(Objects::nonNull).toList());
  130.             }

  131.             var subDepMembers = orgenheter.stream()
  132.                     .map(OrgEnhetDto::getOrganiseringer)
  133.                     .flatMap(Collection::stream)
  134.                     .map(OrganiseringDto::getOrgEnhet)
  135.                     .map(OrgEnhetDto::getLeder)
  136.                     .flatMap(Collection::stream)
  137.                     .map(OrgEnhetsLederDto::getRessurs)
  138.                     .map(RessursDto::getNavident)
  139.                     .filter(Objects::nonNull)
  140.                     .filter(id -> !id.equals(navIdent))
  141.                     .toList();


  142.             var x = UUID.randomUUID();
  143.             log.debug("{}: getLeaderMembers for {}: orgenheter size {}, directMembers size {}, subDepartmentMembers size {}",x, navIdent, orgenheter.size(), directMembers.size(), subDepMembers);
  144.             log.debug("{}\n{}",x,res.getBody().toString());
  145.             return Stream.concat(directMembers.stream(), subDepMembers.stream())
  146.                     .distinct()
  147.                     .toList();
  148.         });
  149.     }

  150.     @SneakyThrows
  151.     public void logErrors(String query, @Nullable Object body) {
  152.         if (body == null) {
  153.             return;
  154.         }
  155.         Method getErrors = requireNonNull(ReflectionUtils.findMethod(body.getClass(), "getErrors"));
  156.         var errors = Optional.ofNullable((ArrayNode) getErrors.invoke(body));
  157.         errors.ifPresent(errorArray -> log.error("Error during graphql query {} {}", query, JsonUtils.toJson(errorArray)));
  158.     }

  159.     private RestOperations template() {
  160.         if (restTemplate == null) {
  161.             restTemplate = restTemplateBuilder
  162.                     .additionalInterceptors(correlationInterceptor(), tokenInterceptor())
  163.                     .messageConverters(new MappingJackson2HttpMessageConverter())
  164.                     .build();
  165.         }
  166.         return restTemplate;
  167.     }

  168.     private boolean ressursHarEnRelevantOrgtilknytning(RessursDto ressursDto, String akseptertOrgenhetId){
  169.         var out = false;
  170.         for(var orgTilknytning : ressursDto.getOrgTilknytning()){
  171.             var idErRelevant = orgTilknytning.getOrgEnhet().getId().equals(akseptertOrgenhetId);
  172.             if(idErRelevant && orgTilknytning.getErDagligOppfolging()){
  173.                 var intervallOkBefore = orgTilknytning.getGyldigFom().isBefore(LocalDate.now().plusDays(1));
  174.                 var intervallOkAfter = (orgTilknytning.getGyldigTom() == null || LocalDate.now().minusDays(1).isBefore(orgTilknytning.getGyldigTom()));
  175.                 out |= intervallOkBefore && intervallOkAfter;
  176.             }
  177.         }
  178.         return out;
  179.     }

  180.     @SneakyThrows
  181.     private ClientHttpRequestInterceptor tokenInterceptor() {
  182.         return (request, body, execution) -> {
  183.             String token = tokenProvider.getConsumerToken(getScope());
  184.             log.debug("tokenInterceptor adding token: %s... for scope '%s'".formatted( (token != null && token.length() > 12 ? token.substring(0,11) : token ), getScope()));
  185.             request.getHeaders().add(HttpHeaders.AUTHORIZATION, token);
  186.             return execution.execute(request, body);
  187.         };
  188.     }

  189.     private String getScope() {
  190.         return scopeTemplate.formatted(securityProperties.isDev() ? "dev" : "prod");
  191.     }
  192. }