DashCacheProvider.java

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

  2. import com.github.benmanes.caffeine.cache.Caffeine;
  3. import com.github.benmanes.caffeine.cache.LoadingCache;
  4. import lombok.RequiredArgsConstructor;
  5. import lombok.extern.slf4j.Slf4j;
  6. import lombok.val;
  7. import no.nav.data.common.utils.StreamUtils;
  8. import no.nav.data.team.cluster.ClusterService;
  9. import no.nav.data.team.cluster.domain.Cluster;
  10. import no.nav.data.team.dashboard.dto.DashResponse;
  11. import no.nav.data.team.location.LocationRepository;
  12. import no.nav.data.team.member.dto.MemberResponse;
  13. import no.nav.data.team.po.ProductAreaService;
  14. import no.nav.data.team.po.domain.ProductArea;
  15. import no.nav.data.team.resource.NomClient;
  16. import no.nav.data.team.resource.domain.ResourceType;
  17. import no.nav.data.team.shared.domain.DomainObjectStatus;
  18. import no.nav.data.team.shared.domain.Member;
  19. import no.nav.data.team.team.TeamService;
  20. import no.nav.data.team.team.domain.*;
  21. import org.springframework.context.annotation.Bean;
  22. import org.springframework.context.annotation.Configuration;

  23. import java.time.DayOfWeek;
  24. import java.time.Duration;
  25. import java.time.LocalDateTime;
  26. import java.util.*;
  27. import java.util.function.BiFunction;
  28. import java.util.stream.Collectors;
  29. import java.util.stream.Stream;

  30. import static no.nav.data.common.utils.StreamUtils.*;

  31. @Configuration
  32. @RequiredArgsConstructor
  33. @Slf4j
  34. public class DashCacheProvider {
  35.     private final ProductAreaService productAreaService;
  36.     private final TeamService teamService;
  37.     private final ClusterService clusterService;
  38.     private final NomClient nomClient;
  39.     private final LocationRepository locationRepository;

  40.     private static final List<Team> E = List.of();
  41.     private static final TreeSet<Integer> groups = new TreeSet<>(Set.of(0, 5, 10, 20, Integer.MAX_VALUE));
  42.     private static final TreeSet<Integer> extPercentGroups = new TreeSet<>(Set.of(0, 25, 50, 75, 100));
  43.     private static final BiFunction<Object, Integer, Integer> counter = (k, v) -> v == null ? 1 : v + 1;

  44.     @Bean(name="dashCache")
  45.     public LoadingCache<String, DashResponse> getDashCache() {
  46.         return Caffeine.newBuilder()
  47.                 .expireAfterWrite(Duration.ofMinutes(1))
  48.                 .maximumSize(1).build(k -> calcDash());
  49.     }

  50.     private DashResponse calcDash() {
  51.         List<Team> teamsActive = teamService.getAllActive();
  52.         List<ProductArea> productAreasActive = productAreaService.getAllActive();
  53.         List<Cluster> clustersActive = clusterService.getAllActive();

  54.         List<Team> teamsAll = teamService.getAll();
  55.         List<ProductArea> productAreasAll = productAreaService.getAll();
  56.         List<Cluster> clustersAll = clusterService.getAll();

  57.         return DashResponse.builder()
  58.                 .teamsCount(teamsActive.size())
  59.                 .productAreasCount(productAreasActive.size())
  60.                 .clusterCount(clustersActive.size())
  61.                 .resources(nomClient.count())
  62.                 .resourcesDb(nomClient.countDb())

  63.                 .teamsCountPlanned(teamsAll.stream().filter(team -> team.getStatus().equals(DomainObjectStatus.PLANNED)).count())
  64.                 .teamsCountInactive(teamsAll.stream().filter(team -> team.getStatus().equals(DomainObjectStatus.INACTIVE)).count())

  65.                 .productAreasCountPlanned(productAreasAll.stream().filter(po -> po.getStatus().equals(DomainObjectStatus.PLANNED)).count())
  66.                 .productAreasCountInactive(productAreasAll.stream().filter(po -> po.getStatus().equals(DomainObjectStatus.INACTIVE)).count())

  67.                 .clusterCountPlanned(clustersAll.stream().filter(cluster -> cluster.getStatus().equals(DomainObjectStatus.PLANNED)).count())
  68.                 .clusterCountInactive(clustersAll.stream().filter(cluster -> cluster.getStatus().equals(DomainObjectStatus.INACTIVE)).count())

  69.                 .total(calcForTotal(teamsActive, productAreasActive, clustersActive))
  70.                 .productAreas(convert(productAreasActive, pa -> calcForArea(filter(teamsActive, t -> pa.getId().equals(t.getProductAreaId())), pa, clustersActive)))
  71.                 .clusters(convert(clustersActive, cluster -> calcForCluster(filter(teamsActive, t -> copyOf(t.getClusterIds()).contains(cluster.getId())), cluster, clustersActive)))

  72.                 .areaSummaryMap(createAreaSummaryMap(teamsActive, productAreasActive, clustersActive))
  73.                 .clusterSummaryMap(createClusterSummaryMap(teamsActive, clustersActive))
  74.                 .teamSummaryMap(createTeamSummaryMap(teamsActive, productAreasActive, clustersActive))

  75.                 .locationSummaryMap(createLocationSummaryMap(teamsActive))

  76.                 .build();
  77.     }


  78.     private <T> void accumulateSubList(HashMap<String, ArrayList<T>> targetMap, String mapKey, List<T> subList ){
  79.         val prev = targetMap.get(mapKey);
  80.         if(prev == null){
  81.             targetMap.put(mapKey,new ArrayList<>(subList));
  82.         }else{
  83.             prev.addAll(subList);
  84.         }
  85.     }

  86.     private <T> long countUnique(List<T> listWithPossibleDuplicates){
  87.         val acc = new ArrayList<T>();
  88.         for(val item : listWithPossibleDuplicates){
  89.             if(!acc.contains(item)) {
  90.                 acc.add(item);
  91.             }
  92.         }
  93.         return acc.size();
  94.     }


  95.     private Map<String, DashResponse.LocationSummary> createLocationSummaryMap(List<Team> teams) {

  96.         val out = new HashMap<String, DashResponse.LocationSummary>();

  97.         val locationToNavIdentList = new HashMap<String, ArrayList<String>>();
  98.         val locationToTeamIdList = new HashMap<String,ArrayList<UUID>>();

  99.         val locationDayToNavIdentList = new HashMap<String,ArrayList<String>>();
  100.         val locationDayToTeamIdList = new HashMap<String,ArrayList<UUID>>();

  101.         for(var team : teams){
  102.             val officeHours = team.getOfficeHours();
  103.             if(officeHours == null) {
  104.                 continue;
  105.             }
  106.             val teamLocCode = officeHours.getLocationCode();

  107.             @SuppressWarnings("OptionalGetWithoutIsPresent")
  108.             val teamLoc = locationRepository.getLocationByCode(teamLocCode).get();
  109.             val teamMemberList = team.getMembers().stream().map(TeamMember::getNavIdent).toList();

  110.             accumulateSubList(locationToNavIdentList,teamLoc.getCode(),teamMemberList);
  111.             accumulateSubList(locationToTeamIdList,teamLoc.getCode(),List.of(team.getId()));
  112.             for(val day : officeHours.getDays()){
  113.                 val mapKeyStr = teamLoc.getCode() + "/" + day.name();
  114.                 accumulateSubList(locationDayToNavIdentList, mapKeyStr, teamMemberList);
  115.                 accumulateSubList(locationDayToTeamIdList, mapKeyStr, List.of(team.getId()));
  116.             }

  117.             var parentLoc = teamLoc.getParent();
  118.             while (parentLoc != null) {
  119.                 val parentLocCode = parentLoc.getCode();
  120.                 accumulateSubList(locationToNavIdentList, parentLocCode, teamMemberList);
  121.                 accumulateSubList(locationToTeamIdList, parentLocCode, List.of(team.getId()));

  122.                 for (val day : officeHours.getDays()) {
  123.                     val mapKeyStr = parentLocCode + "/" + day.name();
  124.                     accumulateSubList(locationDayToNavIdentList, mapKeyStr, teamMemberList);
  125.                     accumulateSubList(locationDayToTeamIdList, mapKeyStr, List.of(team.getId()));
  126.                 }

  127.                 parentLoc = parentLoc.getParent();
  128.             }
  129.         }

  130.         val allLocations = locationRepository.getAll();
  131.         for(val loc : allLocations){

  132.             val locNavIdList = locationToNavIdentList.get(loc.getCode());
  133.             val resCount = locNavIdList != null ? countUnique(locNavIdList) : 0;

  134.             val locTeamIdList = locationToTeamIdList.get(loc.getCode());
  135.             val teamCount = locTeamIdList != null ? countUnique(locTeamIdList) : 0;

  136.             val locSumBuilder = DashResponse.LocationSummary.builder()
  137.                     .resourceCount(resCount)
  138.                     .teamCount( teamCount );

  139.             val weekDays = List.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY);
  140.             for(val day : weekDays){
  141.                 val mapKeyStr = loc.getCode() + "/" + day.name();
  142.                 val locDayNavIdList = locationDayToNavIdentList.get(mapKeyStr);
  143.                 val resCountDay = locDayNavIdList != null ? countUnique(locDayNavIdList) : 0;

  144.                 val locDayTeamIdList = locationDayToTeamIdList.get(mapKeyStr);
  145.                 val teamCountDay = locDayTeamIdList != null ? countUnique(locDayTeamIdList) : 0;

  146.                 switch(day){
  147.                     case MONDAY -> locSumBuilder.monday(new DashResponse.LocationDaySummary(teamCountDay,resCountDay));
  148.                     case TUESDAY -> locSumBuilder.tuesday(new DashResponse.LocationDaySummary(teamCountDay,resCountDay));
  149.                     case WEDNESDAY -> locSumBuilder.wednesday(new DashResponse.LocationDaySummary(teamCountDay,resCountDay));
  150.                     case THURSDAY -> locSumBuilder.thursday(new DashResponse.LocationDaySummary(teamCountDay,resCountDay));
  151.                     case FRIDAY -> locSumBuilder.friday(new DashResponse.LocationDaySummary(teamCountDay,resCountDay));
  152.                 }
  153.             }

  154.             out.put(loc.getCode(),locSumBuilder.build());
  155.         }

  156.         return out;
  157.     }

  158.     private Map<UUID, DashResponse.ClusterSummary> createClusterSummaryMap(List<Team> teams, List<Cluster> clusters) {
  159.         val map = new HashMap<UUID, DashResponse.ClusterSummary>();

  160.         for (val cluster: clusters){

  161.             val relatedTeams = teams.stream()
  162.                     .filter(team -> team.getClusterIds().contains(cluster.getId())
  163.                     ).toList();

  164.             val clusterSubteamMembers = relatedTeams.stream()
  165.                     .flatMap(team -> team.getMembers().stream()).toList();

  166.             val totalMembershipCount = (long) cluster.getMembers().size() + (long) clusterSubteamMembers.size();


  167.             val totaluniqueResources = StreamUtils.distinctByKey(
  168.                     List.of(
  169.                             cluster.getMembers().stream().map(it -> it.getNavIdent()),
  170.                             clusterSubteamMembers.stream().map(it -> it.getNavIdent())

  171.                     ).stream().reduce((a,b) -> Stream.concat(a,b)).get().toList(), it -> it
  172.             );

  173.             val uniqueResourcesExternal = totaluniqueResources.stream()
  174.                     .map(ident -> nomClient.getByNavIdent(ident).orElse(null))
  175.                     .filter(Objects::nonNull)
  176.                     .filter(ressource -> ressource.getResourceType().equals(ResourceType.EXTERNAL))
  177.                     .count();

  178.             map.put(cluster.getId(), DashResponse.ClusterSummary.builder()
  179.                     .totalMembershipCount(totalMembershipCount)
  180.                     .totalUniqueResourcesCount(totaluniqueResources.stream().count())
  181.                     .uniqueResourcesExternal(uniqueResourcesExternal)
  182.                     .teamCount(relatedTeams.stream().count())

  183.                     .build());

  184.         }


  185.         return map;
  186.     }

  187.     private Map<UUID, DashResponse.TeamSummary2> createTeamSummaryMap(List<Team> teams, List<ProductArea> productAreas, List<Cluster> clusters) {
  188.         val map = new HashMap<UUID, DashResponse.TeamSummary2>();

  189.         for(val team : teams){

  190.             val uniqueResourcesExternal = team.getMembers().stream()
  191.                     .map(teamMember -> nomClient.getByNavIdent(teamMember.getNavIdent()).orElse(null))
  192.                     .filter(Objects::nonNull)
  193.                     .filter(resource -> resource.getResourceType().equals(ResourceType.EXTERNAL))
  194.                     .count();


  195.             map.put(team.getId(), DashResponse.TeamSummary2.builder()
  196.                     .membershipCount(team.getMembers().stream().count())
  197.                     .ResourcesExternal(uniqueResourcesExternal).build());



  198.         }

  199.         return map;
  200.     }

  201.     private Map<UUID, DashResponse.AreaSummary> createAreaSummaryMap(List<Team> teams, List<ProductArea> productAreas, List<Cluster> clusters) {
  202.         val map = new HashMap<UUID, DashResponse.AreaSummary>();

  203.         for (val pa: productAreas){

  204.             val relatedClusters = clusters.stream().filter(cl -> pa.getId().equals(cl.getProductAreaId())).toList();



  205.             val relatedTeams = teams.stream().filter(team ->
  206.                     pa.getId().equals(team.getProductAreaId())
  207.             ).toList();
  208.             long clusterCount = relatedClusters.size();

  209.             val relatedClusterMembers = relatedClusters.stream().flatMap(cluster -> {return cluster.getMembers().stream();}).toList();
  210.             val subteamMembers = relatedTeams.stream().flatMap(team -> {return team.getMembers().stream();}).toList();
  211.             val relatedClusterSubteams = relatedClusters.stream()
  212.                     .flatMap(cluster -> teams.stream()
  213.                             .filter(team -> team.getClusterIds().contains(cluster.getId()))
  214.                     ).toList();

  215.             val allSubteams = relatedClusterSubteams.stream().map(it -> it.getId()).collect(Collectors.toSet());
  216.             allSubteams.addAll(relatedTeams.stream().map(it -> it.getId()).collect(Collectors.toSet()));



  217. //            val clusterSubTeamMembers = teams.stream()
  218. //                    .filter(team -> {
  219. //                        val teamBelongsToAreaByCluster = relatedClusters.stream()
  220. //                                .map(cl -> cl.getId())
  221. //                                .anyMatch(clId -> team.getClusterIds().contains(clId));
  222. //                        return teamBelongsToAreaByCluster;
  223. //                    })
  224. //                    .filter(team -> {return (relatedClusterSubteams.stream()
  225. //                        .map(it -> it.getId())
  226. //                        .toList()).contains(team.getId());
  227. //                    }
  228. //                    )
  229. //                    .flatMap(subteam -> {return subteam.getMembers().stream();}).toList();


  230. //            long membershipCount = pa.getMembers().size() + relatedClusterMembers.size() + subteamMembers.size()  + clusterSubTeamMembers.size();
  231.             long membershipCount = pa.getMembers().size() + relatedClusterMembers.size() + subteamMembers.size();

  232.             val uniqueResources = StreamUtils.distinctByKey(
  233.                     List.of(
  234.                             pa.getMembers().stream().map(it -> it.getNavIdent()),
  235.                             relatedClusterMembers.stream().map(it -> it.getNavIdent()),
  236.                             subteamMembers.stream().map(it ->  it.getNavIdent())
  237. //                            clusterSubTeamMembers.stream().map(it -> it.getNavIdent())

  238.                     ).stream().reduce((a,b) -> Stream.concat(a,b)).get().toList(), it -> it
  239.             );

  240.             val uniqueResourcesExternal = uniqueResources.stream()
  241.                     .map(ident -> nomClient.getByNavIdent(ident).orElse(null))
  242.                     .filter(Objects::nonNull)
  243.                     .filter(ressource -> ressource.getResourceType().equals(ResourceType.EXTERNAL))
  244.                     .count();


  245.             map.put(pa.getId(), DashResponse.AreaSummary.builder()
  246.                     .clusterCount(clusterCount)
  247.                     .membershipCount(membershipCount)
  248.                     .uniqueResourcesCount(uniqueResources.stream().count())
  249.                     .totalTeamCount(allSubteams.stream().count())
  250.                     .uniqueResourcesExternal(uniqueResourcesExternal)


  251.                     .build());
  252.         }


  253.         return map;
  254.     }


  255.     private DashResponse.TeamSummary calcForTotal(List<Team> teams, List<ProductArea> productAreas, List<Cluster> clusters) {
  256.         return calcForTeams(teams, null, productAreas, null, clusters);
  257.     }

  258.     private DashResponse.TeamSummary calcForArea(List<Team> teams, ProductArea productArea, List<Cluster> clusters) {
  259.         return calcForTeams(teams, productArea, List.of(), null, clusters);
  260.     }

  261.     private DashResponse.TeamSummary calcForCluster(List<Team> teams, Cluster cluster, List<Cluster> clusters) {
  262.         return calcForTeams(teams, null, List.of(), cluster, clusters);
  263.     }

  264.     private DashResponse.TeamSummary calcForTeams(List<Team> teams, ProductArea productArea, List<ProductArea> productAreas, Cluster cluster, List<Cluster> clusters) {
  265.         Map<TeamRole, Integer> roles = new EnumMap<>(TeamRole.class);
  266.         Map<TeamOwnershipType, Integer> teamOwnershipTypes = new EnumMap<>(TeamOwnershipType.class);
  267.         Map<TeamType, Integer> teamTypes = new EnumMap<>(TeamType.class);

  268.         Map<Integer, List<Team>> teamsBuckets = teams.stream().collect(Collectors.groupingBy(t -> groups.ceiling(t.getMembers().size())));
  269.         Map<Integer, List<Team>> extPercentBuckets = teams.stream().collect(Collectors.groupingBy(t -> extPercentGroups.ceiling(percentExternalMembers(t))));

  270.         teams.stream().flatMap(t -> t.getMembers().stream()).flatMap(m -> m.getRoles().stream()).forEach(r -> roles.compute(r, counter));
  271.         teams.forEach(t -> teamOwnershipTypes.compute(t.getTeamOwnershipType() == null ? TeamOwnershipType.UNKNOWN : t.getTeamOwnershipType(), counter));
  272.         teams.forEach(t -> teamTypes.compute(t.getTeamType() == null ? TeamType.UNKNOWN : t.getTeamType(), counter));

  273.         List<Member> productAreaMembers;
  274.         if (cluster != null) {
  275.             productAreaMembers = List.of();
  276.         } else {
  277.             if (productArea != null) {
  278.                 productAreaMembers = productArea.getMembersAsSuper();
  279.             } else {
  280.                 productAreaMembers = productAreas.stream().flatMap(pa -> pa.getMembers().stream()).collect(Collectors.toList());
  281.             }
  282.         }
  283.         productAreaMembers.stream().flatMap(m -> m.getRoles().stream()).forEach(r -> roles.compute(r, counter));

  284.         List<Member> clusterMembers;
  285.         List<Cluster> paClusters = null;
  286.         if (productArea != null) {
  287.             paClusters = filter(clusters, cl -> productArea.getId().equals(cl.getProductAreaId()));
  288.             clusterMembers = paClusters.stream().flatMap(cl -> cl.getMembers().stream()).collect(Collectors.toList());
  289.         } else {
  290.             if (cluster != null) {
  291.                 clusterMembers = cluster.getMembersAsSuper();
  292.             } else {
  293.                 clusterMembers = clusters.stream().flatMap(cl -> cl.getMembers().stream()).collect(Collectors.toList());
  294.             }
  295.         }
  296.         clusterMembers.stream().flatMap(m -> m.getRoles().stream()).forEach(r -> roles.compute(r, counter));

  297.         return DashResponse.TeamSummary.builder()
  298.                 .productAreaId(productArea != null ? productArea.getId() : null)
  299.                 .clusterId(cluster != null ? cluster.getId() : null)
  300.                 .clusters(paClusters != null ? (long) paClusters.size() : null)
  301.                 .teams(teams.size())
  302.                 .teamsEditedLastWeek(filter(teams, t -> t.getChangeStamp().getLastModifiedDate().isAfter(LocalDateTime.now().minusDays(7))).size())

  303.                 .teamEmpty(teamsBuckets.getOrDefault(0, E).size())
  304.                 .teamUpTo5(teamsBuckets.getOrDefault(5, E).size())
  305.                 .teamUpTo10(teamsBuckets.getOrDefault(10, E).size())
  306.                 .teamUpTo20(teamsBuckets.getOrDefault(20, E).size())
  307.                 .teamOver20(teamsBuckets.getOrDefault(Integer.MAX_VALUE, E).size())

  308.                 .teamExternal0p(extPercentBuckets.getOrDefault(0, E).size())
  309.                 .teamExternalUpto25p(extPercentBuckets.getOrDefault(25, E).size())
  310.                 .teamExternalUpto50p(extPercentBuckets.getOrDefault(50, E).size())
  311.                 .teamExternalUpto75p(extPercentBuckets.getOrDefault(75, E).size())
  312.                 .teamExternalUpto100p(extPercentBuckets.getOrDefault(100, E).size())

  313.                 .uniqueResources(countUniqueResources(teams, productAreaMembers, clusterMembers))
  314.                 .uniqueResourcesExternal(countUniqueResourcesExternal(teams, productAreaMembers, clusterMembers))
  315.                 .totalResources(countResources(teams, productAreaMembers, clusterMembers))

  316.                 .roles(roles.entrySet().stream()
  317.                         .map(e -> new DashResponse.RoleCount(e.getKey(), e.getValue())).collect(Collectors.toList()))
  318.                 .teamOwnershipTypes(teamOwnershipTypes.entrySet().stream()
  319.                         .map(e -> new DashResponse.TeamOwnershipTypeCount(e.getKey(), e.getValue()))
  320.                         .sorted(Comparator.comparing(DashResponse.TeamOwnershipTypeCount::getCount))
  321.                         .collect(Collectors.toList()))
  322.                 .teamTypes(teamTypes.entrySet().stream()
  323.                         .map(e -> new DashResponse.TeamTypeCount(e.getKey(), e.getValue()))
  324.                         .sorted(Comparator.comparing(DashResponse.TeamTypeCount::getCount))
  325.                         .collect(Collectors.toList()))
  326.                 .build();
  327.     }

  328.     private long countUniqueResourcesExternal(List<Team> teams, List<Member> productAreaMembers, List<Member> clusterMembers) {
  329.         return Stream.concat(
  330.                         Stream.concat(
  331.                                 productAreaMembers.stream().map(Member::convertToResponse),
  332.                                 teams.stream().flatMap(team -> team.getMembers().stream()).map(TeamMember::convertToResponse)
  333.                         ),
  334.                         clusterMembers.stream().map(Member::convertToResponse)
  335.                 )
  336.                 .filter(m -> ResourceType.EXTERNAL == m.getResource().getResourceType())
  337.                 .map(MemberResponse::getNavIdent).distinct()
  338.                 .count();
  339.     }

  340.     private long countUniqueResources(List<Team> teams, List<Member> productAreaMembers, List<Member> clusterMembers) {
  341.         return Stream.concat(
  342.                 Stream.concat(
  343.                         productAreaMembers.stream().map(Member::getNavIdent),
  344.                         teams.stream().flatMap(team -> team.getMembers().stream().map(TeamMember::getNavIdent))
  345.                 ), clusterMembers.stream().map(Member::getNavIdent)
  346.         ).distinct().count();

  347.     }

  348.     private long countResources(List<Team> teams, List<Member> productAreaMembers, List<Member> clusterMembers) {
  349.         return teams.stream().mapToLong(team -> team.getMembers().size()).sum() +
  350.                 productAreaMembers.size() + clusterMembers.size();
  351.     }

  352.     private int percentExternalMembers(Team t) {
  353.         if (t.getMembers().isEmpty()) {
  354.             return 0;
  355.         }
  356.         long externalMembers = t.getMembers().stream().map(TeamMember::convertToResponse).filter(m -> ResourceType.EXTERNAL == m.getResource().getResourceType()).count();
  357.         return ((int) externalMembers * 100) / t.getMembers().size();
  358.     }

  359. }