ModelDiffer.java

package com.taxonomy.dsl.diff;

import com.taxonomy.dsl.model.ArchitectureElement;
import com.taxonomy.dsl.model.ArchitectureRelation;
import com.taxonomy.dsl.model.CanonicalArchitectureModel;

import java.util.*;

/**
 * Computes a semantic diff between two {@link CanonicalArchitectureModel} instances.
 *
 * <p>The differ compares elements by their {@code id} and relations by
 * their composite key {@code (sourceId, relationType, targetId)}. This
 * produces a {@link ModelDiff} containing added, removed, and changed
 * items that can be used for incremental materialization.
 *
 * <p>This class is a pure library component — no Spring, no JPA, no
 * external dependencies beyond JDK 17.
 */
public class ModelDiffer {

    /**
     * Compute the diff between two canonical architecture models.
     *
     * @param before the "old" model (may be {@code null} to represent an empty baseline)
     * @param after  the "new" model (may be {@code null} to represent deletion of everything)
     * @return the diff result
     */
    public ModelDiff diff(CanonicalArchitectureModel before, CanonicalArchitectureModel after) {
        CanonicalArchitectureModel effectiveBefore = before != null ? before : new CanonicalArchitectureModel();
        CanonicalArchitectureModel effectiveAfter = after != null ? after : new CanonicalArchitectureModel();

        return new ModelDiff(
                diffAddedElements(effectiveBefore, effectiveAfter),
                diffRemovedElements(effectiveBefore, effectiveAfter),
                diffChangedElements(effectiveBefore, effectiveAfter),
                diffAddedRelations(effectiveBefore, effectiveAfter),
                diffRemovedRelations(effectiveBefore, effectiveAfter),
                diffChangedRelations(effectiveBefore, effectiveAfter)
        );
    }

    // ── Element diffing ──────────────────────────────────────────────

    private List<ArchitectureElement> diffAddedElements(CanonicalArchitectureModel before,
                                                         CanonicalArchitectureModel after) {
        Set<String> beforeIds = before.allElementIds();
        List<ArchitectureElement> added = new ArrayList<>();
        for (ArchitectureElement e : after.getElements()) {
            if (!beforeIds.contains(e.getId())) {
                added.add(e);
            }
        }
        return added;
    }

    private List<ArchitectureElement> diffRemovedElements(CanonicalArchitectureModel before,
                                                           CanonicalArchitectureModel after) {
        Set<String> afterIds = after.allElementIds();
        List<ArchitectureElement> removed = new ArrayList<>();
        for (ArchitectureElement e : before.getElements()) {
            if (!afterIds.contains(e.getId())) {
                removed.add(e);
            }
        }
        return removed;
    }

    private List<ModelDiff.ElementChange> diffChangedElements(CanonicalArchitectureModel before,
                                                               CanonicalArchitectureModel after) {
        Map<String, ArchitectureElement> beforeMap = indexElementsById(before);
        List<ModelDiff.ElementChange> changed = new ArrayList<>();
        for (ArchitectureElement afterElem : after.getElements()) {
            ArchitectureElement beforeElem = beforeMap.get(afterElem.getId());
            if (beforeElem != null && !elementsEqual(beforeElem, afterElem)) {
                changed.add(new ModelDiff.ElementChange(beforeElem, afterElem));
            }
        }
        return changed;
    }

    // ── Relation diffing ─────────────────────────────────────────────

    private List<ArchitectureRelation> diffAddedRelations(CanonicalArchitectureModel before,
                                                           CanonicalArchitectureModel after) {
        Set<String> beforeKeys = indexRelationKeys(before);
        List<ArchitectureRelation> added = new ArrayList<>();
        for (ArchitectureRelation r : after.getRelations()) {
            if (!beforeKeys.contains(relationKey(r))) {
                added.add(r);
            }
        }
        return added;
    }

    private List<ArchitectureRelation> diffRemovedRelations(CanonicalArchitectureModel before,
                                                             CanonicalArchitectureModel after) {
        Set<String> afterKeys = indexRelationKeys(after);
        List<ArchitectureRelation> removed = new ArrayList<>();
        for (ArchitectureRelation r : before.getRelations()) {
            if (!afterKeys.contains(relationKey(r))) {
                removed.add(r);
            }
        }
        return removed;
    }

    private List<ModelDiff.RelationChange> diffChangedRelations(CanonicalArchitectureModel before,
                                                                 CanonicalArchitectureModel after) {
        Map<String, ArchitectureRelation> beforeMap = indexRelationsByKey(before);
        List<ModelDiff.RelationChange> changed = new ArrayList<>();
        for (ArchitectureRelation afterRel : after.getRelations()) {
            ArchitectureRelation beforeRel = beforeMap.get(relationKey(afterRel));
            if (beforeRel != null && !relationsEqual(beforeRel, afterRel)) {
                changed.add(new ModelDiff.RelationChange(beforeRel, afterRel));
            }
        }
        return changed;
    }

    // ── Equality helpers ─────────────────────────────────────────────

    private boolean elementsEqual(ArchitectureElement a, ArchitectureElement b) {
        return Objects.equals(a.getId(), b.getId())
                && Objects.equals(a.getType(), b.getType())
                && Objects.equals(a.getTitle(), b.getTitle())
                && Objects.equals(a.getDescription(), b.getDescription())
                && Objects.equals(a.getTaxonomy(), b.getTaxonomy())
                && Objects.equals(a.getExtensions(), b.getExtensions());
    }

    private boolean relationsEqual(ArchitectureRelation a, ArchitectureRelation b) {
        return Objects.equals(a.getSourceId(), b.getSourceId())
                && Objects.equals(a.getRelationType(), b.getRelationType())
                && Objects.equals(a.getTargetId(), b.getTargetId())
                && Objects.equals(a.getStatus(), b.getStatus())
                && Objects.equals(a.getConfidence(), b.getConfidence())
                && Objects.equals(a.getProvenance(), b.getProvenance())
                && Objects.equals(a.getExtensions(), b.getExtensions());
    }

    // ── Indexing helpers ─────────────────────────────────────────────

    private Map<String, ArchitectureElement> indexElementsById(CanonicalArchitectureModel model) {
        Map<String, ArchitectureElement> map = new LinkedHashMap<>();
        for (ArchitectureElement e : model.getElements()) {
            map.put(e.getId(), e);
        }
        return map;
    }

    /**
     * Composite key for a relation: {@code sourceId|relationType|targetId}.
     */
    private String relationKey(ArchitectureRelation r) {
        return r.getSourceId() + "|" + r.getRelationType() + "|" + r.getTargetId();
    }

    private Set<String> indexRelationKeys(CanonicalArchitectureModel model) {
        Set<String> keys = new LinkedHashSet<>();
        for (ArchitectureRelation r : model.getRelations()) {
            keys.add(relationKey(r));
        }
        return keys;
    }

    private Map<String, ArchitectureRelation> indexRelationsByKey(CanonicalArchitectureModel model) {
        Map<String, ArchitectureRelation> map = new LinkedHashMap<>();
        for (ArchitectureRelation r : model.getRelations()) {
            map.put(relationKey(r), r);
        }
        return map;
    }
}