RelationTraversalService.java
package com.taxonomy.relations.service;
import com.taxonomy.catalog.model.TaxonomyNode;
import com.taxonomy.catalog.model.TaxonomyRelation;
import com.taxonomy.catalog.repository.TaxonomyRelationRepository;
import com.taxonomy.catalog.service.TaxonomyRelationService;
import com.taxonomy.dto.TaxonomyRelationDto;
import com.taxonomy.dsl.model.TaxonomyRootTypes;
import com.taxonomy.model.RelationType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* Loads and filters traversable relations for the architecture view.
* Only whitelisted relation types are returned.
*
* <p>Hierarchy-aware: when a leaf node (e.g. "CO-1023") has no direct
* relations, the service also returns relations defined for its root
* code ("CO"). This ensures leaf-level anchors inherit the architecture
* relations seeded at the root level.</p>
*/
@Service
public class RelationTraversalService {
/** Relation types allowed for propagation in their canonical traversal order. */
static final List<RelationType> WHITELISTED_TYPES = List.of(
RelationType.SUPPORTS,
RelationType.REALIZES,
RelationType.USES,
RelationType.FULFILLS,
RelationType.DEPENDS_ON
);
private static final Map<RelationType, Integer> RELATION_TYPE_ORDER = Map.of(
RelationType.SUPPORTS, 10,
RelationType.REALIZES, 20,
RelationType.USES, 30,
RelationType.FULFILLS, 40,
RelationType.DEPENDS_ON, 50);
/**
* Canonical architectural layer order used only as a deterministic tie breaker.
* Values are intentionally distinct even where diagram layers are visually shared.
*/
private static final Map<String, Integer> ROOT_ORDER = Map.of(
"CP", 10,
"BP", 20,
"BR", 21,
"CR", 30,
"CI", 31,
"UA", 40,
"IP", 50,
"CO", 60);
/**
* Total semantic ordering independent of database plans and generated row IDs.
*
* <p>Equal propagation paths can otherwise make the selected explanation and
* derived impact-edge order depend on the database's physical scan order. IDs
* are retained only as a final tie breaker for genuinely duplicate in-memory
* objects, not as the architecture semantics.</p>
*/
private static final Comparator<TaxonomyRelation> RELATION_ORDER = Comparator
.comparingInt((TaxonomyRelation relation) ->
RELATION_TYPE_ORDER.getOrDefault(relation.getRelationType(), Integer.MAX_VALUE))
.thenComparingInt(relation -> rootOrder(sourceCode(relation)))
.thenComparingInt(relation -> rootOrder(targetCode(relation)))
.thenComparing(RelationTraversalService::sourceCode,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(RelationTraversalService::targetCode,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(TaxonomyRelation::getId,
Comparator.nullsLast(Comparator.naturalOrder()));
private final TaxonomyRelationRepository relationRepository;
private final TaxonomyRelationService relationService;
public RelationTraversalService(TaxonomyRelationRepository relationRepository,
TaxonomyRelationService relationService) {
this.relationRepository = relationRepository;
this.relationService = relationService;
}
/**
* Returns all traversable relations for a given node code,
* considering both outgoing and incoming (for bidirectional) relations
* filtered to the whitelisted types.
*
* <p>If the node is a leaf code (e.g. "CO-1023") with no direct relations,
* also includes relations from its taxonomy root code ("CO").</p>
*/
@Transactional(readOnly = true)
public List<TaxonomyRelationDto> getTraversableRelations(String nodeCode) {
List<TaxonomyRelationDto> result = new ArrayList<>();
addRelationsFor(nodeCode, result);
// Hierarchy fallback: also include root-level relations for leaf nodes
if (result.isEmpty()) {
String rootCode = TaxonomyRootTypes.rootFromId(nodeCode);
if (rootCode != null && !rootCode.equals(nodeCode)) {
addRelationsFor(rootCode, result);
}
}
return result;
}
/** Returns all relations of the whitelisted types in canonical semantic order. */
@Transactional(readOnly = true)
public List<TaxonomyRelationDto> getAllTraversableRelations() {
List<TaxonomyRelation> relations = new ArrayList<>(
relationRepository.findByRelationTypeIn(WHITELISTED_TYPES));
relations.sort(RELATION_ORDER);
List<TaxonomyRelationDto> dtos = new ArrayList<>(relations.size());
for (TaxonomyRelation relation : relations) {
dtos.add(relationService.toDto(relation));
}
return dtos;
}
private void addRelationsFor(String code, List<TaxonomyRelationDto> result) {
List<TaxonomyRelation> outgoing = new ArrayList<>(
relationRepository.findBySourceNodeCodeAndRelationTypeIn(
code, WHITELISTED_TYPES));
outgoing.sort(RELATION_ORDER);
for (TaxonomyRelation relation : outgoing) {
result.add(relationService.toDto(relation));
}
List<TaxonomyRelation> incoming = new ArrayList<>(
relationRepository.findByTargetNodeCodeAndRelationTypeIn(
code, WHITELISTED_TYPES));
incoming.sort(RELATION_ORDER);
for (TaxonomyRelation relation : incoming) {
if (relation.isBidirectional()) {
result.add(relationService.toDto(relation));
}
}
}
private static int rootOrder(String code) {
String root = rootOf(code);
return ROOT_ORDER.getOrDefault(root, Integer.MAX_VALUE);
}
private static String rootOf(String code) {
if (code == null) {
return null;
}
int separator = code.indexOf('-');
return separator >= 0 ? code.substring(0, separator) : code;
}
private static String sourceCode(TaxonomyRelation relation) {
TaxonomyNode source = relation.getSourceNode();
return source != null ? source.getCode() : null;
}
private static String targetCode(TaxonomyRelation relation) {
TaxonomyNode target = relation.getTargetNode();
return target != null ? target.getCode() : null;
}
}