ProductAreaService.java
package no.nav.data.team.po;
import lombok.extern.slf4j.Slf4j;
import no.nav.data.common.exceptions.ValidationException;
import no.nav.data.common.storage.StorageService;
import no.nav.data.common.storage.domain.GenericStorage;
import no.nav.data.common.validator.Validator;
import no.nav.data.team.cluster.ClusterRepository;
import no.nav.data.team.org.OrgService;
import no.nav.data.team.po.domain.AreaType;
import no.nav.data.team.po.domain.PaMember;
import no.nav.data.team.po.domain.ProductArea;
import no.nav.data.team.po.dto.AddTeamsToProductAreaRequest;
import no.nav.data.team.po.dto.PaMemberRequest;
import no.nav.data.team.po.dto.ProductAreaRequest;
import no.nav.data.team.shared.domain.DomainObjectStatus;
import no.nav.data.team.team.TeamRepository;
import no.nav.data.team.team.domain.Team;
import no.nav.data.team.team.dto.TeamRequest.Fields;
import no.nav.nom.graphql.model.OrgEnhetDto;
import no.nav.nom.graphql.model.OrganiseringDto;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import static java.util.Objects.nonNull;
import static java.util.stream.Collectors.groupingBy;
import static no.nav.data.common.utils.StreamUtils.convert;
import static no.nav.data.common.validator.Validator.ERROR_MESSAGE_MISSING;
import static no.nav.data.common.validator.Validator.ILLEGAL_ARGUMENT;
@Slf4j
@Service
public class ProductAreaService {
private final StorageService storage;
private final TeamRepository teamRepository;
private final ClusterRepository clusterRepository;
private final ProductAreaRepository repository;
private final OrgService orgService;
public ProductAreaService(StorageService storage, TeamRepository teamRepository,
ClusterRepository clusterRepository, ProductAreaRepository repository, OrgService orgService) {
this.storage = storage;
this.teamRepository = teamRepository;
this.clusterRepository = clusterRepository;
this.repository = repository;
this.orgService = orgService;
}
@Scheduled(cron = "0 0 6,11,17,23 * * *")
private void updateOwnerGroup() {
List<ProductAreaRequest> allProductAreasAsRequests =
repository.findAllProductAreas().stream()
.map(GenericStorage::toProductArea)
.filter(productArea -> productArea.getAreaType().equals(AreaType.PRODUCT_AREA))
.filter(productArea -> productArea.getStatus().isActive())
.filter(productArea -> nonNull(productArea.getNomId()))
.map(ProductAreaRequest::convertToRequest)
.toList();
allProductAreasAsRequests.forEach(request -> {
try{
save(request);
} catch (ValidationException ex) {
log.warn("ValidationException, Failed to update product area " + request.getId(), ex);
} catch (RuntimeException ex) {
log.warn("Misc exception, Failed to update product area " + request.getId(), ex);
}
});
}
@EventListener
private void updateOwnerGroupOnStartup(AvailabilityChangeEvent<ReadinessState> event) {
var ready = event.getState().equals(ReadinessState.ACCEPTING_TRAFFIC);
if (ready) {
updateOwnerGroup();
}
}
public ProductArea save(ProductAreaRequest request) {
Validator.validate(request, storage)
.addValidations(this::validateArbeidsomraade)
.addValidations(this::validateName)
.addValidations(this::validateStatusNotNull)
.addValidations(this::validateProductAreaMemberRoleOk)
.addValidations(this::validateProductMemberAreaNoDuplicates)
.ifErrorsThrowValidationException();
var productArea = request.isUpdate() ? storage.get(request.getIdAsUUID(), ProductArea.class) : new ProductArea();
var members = request.getMembers() == null ? productArea.getMembers() : request.getMembers().stream().map(PaMember::convert).toList();
if (request.getAreaType().equals(AreaType.PRODUCT_AREA)) {
members = determineMembersToPersistForProductArea(request, productArea);
}else if (members == null){
members = List.of(); // if current (prior to update) members are not present, start with empty list
}
var avdelingNomId = orgService.getAvdelingNomId(request.getNomId());
productArea.setFieldsFromRequest(request, avdelingNomId, members);
return storage.save(productArea);
}
private void validateProductMemberAreaNoDuplicates(Validator<ProductAreaRequest> productAreaRequestValidator) {
var req = productAreaRequestValidator.getItem();
if(req.getMembers() == null){
return;
}
var duplicateNavIdentEntries = req.getMembers().stream().collect(Collectors.groupingBy(PaMemberRequest::getNavIdent, Collectors.counting()))
.entrySet().stream()
.filter(y -> y.getValue() > 1)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (!duplicateNavIdentEntries.isEmpty()) {
var errMsg = duplicateNavIdentEntries.entrySet().stream().map(it -> it.getKey() + ":" + it.getValue()).collect(Collectors.joining(", "));
productAreaRequestValidator.addError(Fields.members, ILLEGAL_ARGUMENT, "Cannot accept duplicate navident entries: " + errMsg);
}
}
private void validateProductAreaMemberRoleOk(Validator<ProductAreaRequest> validator) {
var members = validator.getItem().getMembers();
if (members == null) return;
for (var member : members) {
var roles = member.getRoles();
if (roles == null) continue;
for (var role : roles) {
if (role.isLeaderGroupRole()) {
validator.addError("members", ILLEGAL_ARGUMENT, String.format("Role '%s' is not applicable for seksjon member", role));
}
}
}
}
private void validateArbeidsomraade(Validator<ProductAreaRequest> productAreaRequestValidator) {
if (productAreaRequestValidator.getItem().getAreaType().equals(AreaType.PRODUCT_AREA)
&& productAreaRequestValidator.getItem().getNomId() != null
&& !orgService.isOrgEnhetInArbeidsomraadeOgDirektorat(productAreaRequestValidator.getItem().getNomId())) {
productAreaRequestValidator.addError("status", ILLEGAL_ARGUMENT, "Product area must be in arbeidsomraade and directorate");
}
}
private void validateStatusNotNull(Validator<ProductAreaRequest> productAreaRequestValidator) {
if (productAreaRequestValidator.getItem().getStatus() == null) {
productAreaRequestValidator.addError("status", ILLEGAL_ARGUMENT, "Status cannot be null");
}
}
public ProductArea get(UUID id) {
return storage.get(id, ProductArea.class);
}
public ProductArea getByNomId(String id) {
return repository.findByNomId(id).map(GenericStorage::toProductArea).orElse(null);
}
public List<ProductArea> search(String name) {
return convert(repository.findByNameLike(name), GenericStorage::toProductArea);
}
public ProductArea delete(UUID id) {
List<GenericStorage> teams = teamRepository.findByProductArea(id);
if (!teams.isEmpty()) {
String message = "Cannot delete product area, it is in use by " + teams.size() + " teams";
log.debug(message);
throw new ValidationException(message);
}
List<GenericStorage> clusters = clusterRepository.findByProductArea(id);
if (!clusters.isEmpty()) {
String message = "Cannot delete product area, it is in use by " + clusters.size() + " clusters";
log.debug(message);
throw new ValidationException(message);
}
return storage.delete(id, ProductArea.class);
}
public List<ProductArea> getAll() {
return storage.getAll(ProductArea.class);
}
public List<ProductArea> getAllActive() {
return getAll().stream().filter(po -> po.getStatus() == DomainObjectStatus.ACTIVE).toList();
}
public void addTeams(AddTeamsToProductAreaRequest request) {
Validator.validate(request)
.addValidations(validator -> validator.checkExists(request.getProductAreaId(), storage, ProductArea.class))
.addValidations(AddTeamsToProductAreaRequest::getTeamIds, (validator, teamId) -> validator.checkExists(teamId, storage, Team.class))
.ifErrorsThrowValidationException();
request.getTeamIds().forEach(teamId -> {
var team = storage.get(UUID.fromString(teamId), Team.class);
team.setProductAreaId(request.productAreaIdAsUUID());
team.setUpdateSent(false);
storage.save(team);
});
}
private void validateName(Validator<ProductAreaRequest> validator) {
String name = validator.getItem().getName();
if (name == null || name.isEmpty()) {
validator.addError(Fields.name, ERROR_MESSAGE_MISSING, "Name is required");
}
}
private List<PaMember> determineMembersToPersistForProductArea(ProductAreaRequest request, ProductArea previousProductArea) {
var orgEnhetDtos = orgService.getOrgEnhetOgUnderEnheter(request.getNomId());
ProductAreaMemberAccumulator.Organizational organizational = null;
if (orgEnhetDtos != null) {
// todo, just throw error in the opposite case, if nomID is essentially invalid?
var lederNavident = orgEnhetDtos.getLedere().getFirst().getRessurs().getNavident();
var underEnheter = orgEnhetDtos.getOrganiseringer().stream()
.map(OrganiseringDto::getOrgEnhet).toList();
var ledereOgOrgEnhetNavn = underEnheter.stream()
.collect(groupingBy(
underEnhetDto -> underEnhetDto.getLedere().getFirst().getRessurs().getNavident(),
Collectors.mapping(OrgEnhetDto::getNavn, Collectors.toList())
));
organizational = new ProductAreaMemberAccumulator.Organizational(lederNavident, ledereOgOrgEnhetNavn);
}
var maybeRequestMembers = request.getMembers() == null ? new ArrayList<PaMemberRequest>() : request.getMembers();
var paMembersInRequest = maybeRequestMembers.stream().map(PaMember::convert).toList();
return ProductAreaMemberAccumulator
.accumulate(
new ProductAreaMemberAccumulator.Original(previousProductArea.getMembers(), previousProductArea.getOwnerGroupNavidentList()),
new ProductAreaMemberAccumulator.Updated(paMembersInRequest, request.getOwnerGroupNavidentList()),
organizational
)
.membersToPersist();
}
}