IntegrationDiff.java

package com.taxonomy.interop;

import com.taxonomy.extension.api.integration.IntegrationContracts.*;
import com.taxonomy.interop.persistence.IntegrationStore.Identity;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

/** Three-way field comparison. No timestamps, transport outcomes or list omissions choose an automatic winner. */
@Component
public class IntegrationDiff {
    private final IntegrationJson json;
    public IntegrationDiff(IntegrationJson json) { this.json = json; }
    public List<IntegrationChange> compare(ExchangeDocument incoming, AuthorityMode mode, List<Identity> mappings, Map<String, Artifact> current) {
        Map<String, Identity> baselines = new TreeMap<>(); mappings.forEach(m -> baselines.put(m.externalId(), m));
        Map<String, Artifact> external = ExchangeItems.flatten(incoming);
        TreeSet<String> ids = new TreeSet<>(external.keySet()); ids.addAll(baselines.keySet());
        List<IntegrationChange> changes = new ArrayList<>();
        for (String id : ids) {
            Identity baseline = baselines.get(id); Artifact next = external.get(id), local = current.get(id);
            if (next == null) {
                if (baseline == null || baseline.external() == null || baseline.removed() || !incoming.completeScope()
                        || (mode != AuthorityMode.MIRROR_READ && mode != AuthorityMode.BIDIRECTIONAL)) continue;
                List<String> conflict = changed(baseline.internal(), local).isEmpty() ? List.of() : List.of("DELETE_VERSUS_LOCAL_CHANGE");
                changes.add(change(id, conflict.isEmpty() ? ChangeKind.REMOVE_CANDIDATE : ChangeKind.CONFLICT, Set.of("removed"), local, baseline.external(), null, conflict));
                continue;
            }
            if (baseline == null || baseline.removed()) {
                List<String> conflicts = baseline != null ? List.of("EXTERNAL_IDENTITY_REUSED") : local != null ? List.of("DUPLICATE_MAPPING") : List.of();
                changes.add(change(id, conflicts.isEmpty() ? ChangeKind.ADD : ChangeKind.CONFLICT, ExchangeItems.fields(next).keySet(), local, baseline == null ? null : baseline.external(), next, conflicts));
                continue;
            }
            // A delivered identity is bound, but has no confirmed external baseline yet.
            // Compare its return against the frozen export without inferring deletion from omissions.
            Artifact observed = baseline.external() == null ? baseline.internal() : baseline.external();
            Set<String> remoteFields = changed(observed, next);
            Set<String> localFields = changed(baseline.internal(), local);
            List<String> conflicts = new ArrayList<>();
            for (String field : remoteFields) if (localFields.contains(field)
                    && !Objects.equals(ExchangeItems.fields(local).get(field), ExchangeItems.fields(next).get(field))) conflicts.add(field);
            if (local == null && !remoteFields.isEmpty()) conflicts.add("LOCAL_DELETE_VERSUS_EXTERNAL_CHANGE");
            ChangeKind kind = !conflicts.isEmpty() ? ChangeKind.CONFLICT : remoteFields.isEmpty() ? ChangeKind.UNCHANGED
                    : next.kind() == ArtifactKind.PLACEMENT ? ChangeKind.MOVE : next.kind() == ArtifactKind.RELATION ? ChangeKind.RELATION : ChangeKind.UPDATE;
            changes.add(change(id, kind, remoteFields, local, baseline.external(), next, conflicts));
        }
        return List.copyOf(changes);
    }
    private IntegrationChange change(String id, ChangeKind kind, Set<String> fields, Artifact local, Artifact external, Artifact next, List<String> conflicts) {
        return new IntegrationChange(id, id, kind, fields, json.fingerprint(ExchangeItems.fields(local)), json.fingerprint(ExchangeItems.fields(external)), local, next, conflicts);
    }
    public static Set<String> changed(Artifact before, Artifact after) {
        Map<String, String> left = ExchangeItems.fields(before), right = ExchangeItems.fields(after);
        Set<String> keys = new TreeSet<>(left.keySet()); keys.addAll(right.keySet()); keys.removeIf(key -> Objects.equals(left.get(key), right.get(key))); return keys;
    }
}