AuditDiffService.java
package no.nav.data.team.notify;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import no.nav.data.common.auditing.domain.Action;
import no.nav.data.common.auditing.domain.AuditVersionRepository;
import no.nav.data.common.auditing.dto.AuditMetadata;
import no.nav.data.team.notify.domain.Notification;
import no.nav.data.team.notify.domain.Notification.NotificationChannel;
import no.nav.data.team.notify.domain.Notification.NotificationType;
import no.nav.data.team.notify.domain.NotificationTask;
import no.nav.data.team.notify.domain.NotificationTask.AuditTarget;
import no.nav.data.team.notify.domain.TeamAuditMetadata;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Stream;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static no.nav.data.common.utils.StreamUtils.convert;
import static no.nav.data.common.utils.StreamUtils.filter;
import static no.nav.data.common.utils.StreamUtils.find;
import static no.nav.data.common.utils.StreamUtils.tryFind;
import static no.nav.data.common.utils.StreamUtils.union;
@Slf4j
@Service
public class AuditDiffService {
private final AuditVersionRepository auditVersionRepository;
public AuditDiffService(AuditVersionRepository auditVersionRepository) {
this.auditVersionRepository = auditVersionRepository;
}
public List<NotificationTask> createTask(List<AuditMetadata> audits, List<Notification> notifications) {
var allTasks = new ArrayList<NotificationTask>();
if (audits.isEmpty()) {
return allTasks;
}
var lastAudit = audits.get(audits.size() - 1);
var auditsStart = audits.get(0).getTime();
var auditsEnd = lastAudit.getTime();
log.info("Notification {} audits", audits.size());
var teamsPrev = auditVersionRepository.getTeamMetadataBefore(auditsStart);
var teamsCurr = auditVersionRepository.getTeamMetadataBetween(auditsStart, auditsEnd);
notifications = expandProductAreaNotifications(notifications, teamsPrev, teamsCurr);
var auditsByTargetId = audits.stream().collect(groupingBy(AuditMetadata::getTableId));
notifications.removeIf(n -> {
boolean notAllEventNotification = n.getType() != NotificationType.ALL_EVENTS;
boolean noAuditsForNotification = !auditsByTargetId.containsKey(n.getTarget());
boolean noDependentAuditsForNotification = auditsByTargetId.keySet().stream().noneMatch(n::isDependentOn);
return notAllEventNotification && noAuditsForNotification && noDependentAuditsForNotification;
}
);
notifications.forEach(n -> {
if (n.getTarget() != null && !auditsByTargetId.containsKey(n.getTarget())) {
log.info("Adding empty audits for target {}", n.getTarget());
auditsByTargetId.put(n.getTarget(), List.of());
}
});
var notificationsByIdent = notifications.stream().collect(groupingBy(Notification::getIdent));
log.info("Notification for {}", notificationsByIdent.keySet());
for (Entry<String, List<Notification>> entry : notificationsByIdent.entrySet()) {
String ident = entry.getKey();
List<Notification> notificationsForIdent = entry.getValue();
var notificationTargetAudits = unpackAndGroupTargets(notificationsForIdent, auditsByTargetId);
var tasksForIdent = createTasks(ident, notificationTargetAudits);
allTasks.addAll(filter(tasksForIdent, t -> !t.getTargets().isEmpty()));
}
return allTasks;
}
private List<Notification> expandProductAreaNotifications(List<Notification> notifications, List<TeamAuditMetadata> teamsPrev, List<TeamAuditMetadata> teamsCurr) {
var allNotifications = new ArrayList<>(notifications);
for (Notification notification : notifications) {
if (notification.getType() == NotificationType.PA) {
var paTeamsPrev = filter(teamsPrev, t -> notification.getTarget().equals(t.getProductAreaId()));
var paTeamsCurr = filter(teamsCurr, t -> notification.getTarget().equals(t.getProductAreaId()));
var allTeams = union(paTeamsPrev, paTeamsCurr).stream().map(TeamAuditMetadata::getTableId).distinct().collect(toList());
// Adding teams from product area to notifications, setting their level as Product area, to enforce correct channel overrides later
allNotifications.addAll(convert(allTeams, teamId -> Notification.builder()
.type(NotificationType.PA)
.time(notification.getTime())
.ident(notification.getIdent())
.channels(notification.getChannels())
.target(teamId)
.build()));
notification.setDependentTargets(allTeams);
log.info("Notification PA {} DependentTargets {}", notification.getTarget(), allTeams);
}
}
return allNotifications;
}
private List<NotificationTargetAudits> unpackAndGroupTargets(List<Notification> notifications, Map<UUID, List<AuditMetadata>> auditsByTargetId) {
var allEventAudits = new ArrayList<NotificationTargetAudits>();
// unpack ALL_EVENTS
tryFind(notifications, n -> n.getType() == NotificationType.ALL_EVENTS)
.ifPresent(n -> allEventAudits.addAll(convert(auditsByTargetId.entrySet(), e -> new NotificationTargetAudits(n, e.getKey(), e.getValue()))));
var targetAudits = notifications.stream()
.filter(n -> n.getType() != NotificationType.ALL_EVENTS)
.map(n -> new NotificationTargetAudits(n, n.getTarget(), auditsByTargetId.get(n.getTarget())))
.collect(toList());
return union(allEventAudits, targetAudits);
}
private List<NotificationTask> createTasks(String ident, List<NotificationTargetAudits> targetAudits) {
// All times are equal down here
var time = targetAudits.get(0).getNotification().getTime();
// Calculate which type is the most specific for each target.
// ie. if a a user has a team marked as a different channel than it's product area, we will use the teams channel settings for that target, same goes for ALL_EVENTS.
Map<UUID, NotificationType> targetTypes = new HashMap<>();
targetAudits.forEach(ta -> targetTypes.compute(ta.getTargetId(), (uuid, existingType) -> NotificationType.min(ta.getNotification().getType(), existingType)));
var classifications = Stream.of(NotificationChannel.values()).map(TargetClassification::new).collect(toList());
targetAudits.forEach(ta -> {
Notification notification = ta.getNotification();
UUID targetId = ta.getTargetId();
var notificationTypeForTarget = targetTypes.get(targetId);
var silent = notificationTypeForTarget != notification.getType();
filter(classifications, c -> c.matches(notification.getChannels()))
.forEach(c -> c.add(new Target(targetId, ta.getAudits(), silent)));
});
return classifications.stream()
.filter(c -> c.getTargets().stream().anyMatch(t -> !t.isSilent()))
.map(classification ->
NotificationTask.builder()
.ident(ident)
.time(time)
.channel(classification.getChannel())
.targets(convertAuditTargets(ident, classification.getTargets()))
.build()
)
.collect(toList());
}
private List<AuditTarget> convertAuditTargets(String ident, List<Target> targets) {
return convert(targets, target -> {
var targetId = target.getTarget();
var audits = target.getAudits();
AuditMetadata oldestAudit;
UUID prev;
UUID curr;
if (audits.isEmpty()) {
// If the target in question has not actually changed, ie. a team added/removed in a product area
oldestAudit = auditVersionRepository.lastAuditForObject(targetId);
prev = oldestAudit.getId();
curr = oldestAudit.getId();
} else {
oldestAudit = audits.get(0);
var newestAudit = audits.get(audits.size() - 1);
prev = getPreviousFor(oldestAudit);
curr = newestAudit.getAction() == Action.DELETE ? null : newestAudit.getId();
}
if (prev == null && curr == null) {
log.info("Create and delete target {}, ignoring", targetId);
return null;
}
var tableName = oldestAudit.getTableName();
log.info("Notification to {} target {}: {} from {} to {}", ident, tableName, targetId, prev, curr);
return AuditTarget.builder()
.targetId(targetId)
.type(tableName)
.prevAuditId(prev)
.currAuditId(curr)
.silent(target.isSilent())
.build();
});
}
private UUID getPreviousFor(AuditMetadata oldestAudit) {
if (oldestAudit.getAction() == Action.CREATE) {
return null;
}
return UUID.fromString(auditVersionRepository.getPreviousAuditIdFor(oldestAudit.getId()));
}
@Value
static class NotificationTargetAudits {
Notification notification;
UUID targetId;
List<AuditMetadata> audits;
}
@Value
static class TargetClassification {
NotificationChannel channel;
List<Target> targets = new ArrayList<>();
Set<UUID> targetsAdded = new HashSet<>();
TargetClassification(NotificationChannel channel) {
this.channel = channel;
}
boolean isAdded(UUID targetId) {
return targetsAdded.contains(targetId);
}
void add(Target target) {
if (isAdded(target.getTarget())) {
if (!target.isSilent()) {
var existingTarget = find(targets, t -> t.getTarget().equals(target.getTarget()));
if (existingTarget.isSilent()) {
targets.remove(existingTarget);
targets.add(target);
}
}
} else {
targetsAdded.add(target.getTarget());
targets.add(target);
}
}
boolean matches(List<NotificationChannel> channels) {
return channels.contains(channel);
}
}
@Value
static class Target {
UUID target;
List<AuditMetadata> audits;
boolean silent;
}
}