RelationTraversalService.java

package com.taxonomy.relations.service;

import com.taxonomy.dto.TaxonomyRelationDto;
import com.taxonomy.dsl.model.TaxonomyRootTypes;
import com.taxonomy.model.RelationType;
import com.taxonomy.catalog.model.TaxonomyRelation;
import com.taxonomy.catalog.repository.TaxonomyRelationRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import com.taxonomy.catalog.service.TaxonomyRelationService;

/**
 * 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.
 */
@Service
public class RelationTraversalService {

    /** Relation types allowed for propagation. */
    static final List<RelationType> WHITELISTED_TYPES = List.of(
            RelationType.SUPPORTS,
            RelationType.REALIZES,
            RelationType.USES,
            RelationType.FULFILLS,
            RelationType.DEPENDS_ON
    );

    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").
     */
    @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.
     */
    @Transactional(readOnly = true)
    public List<TaxonomyRelationDto> getAllTraversableRelations() {
        List<TaxonomyRelation> relations = relationRepository.findByRelationTypeIn(WHITELISTED_TYPES);
        List<TaxonomyRelationDto> dtos = new ArrayList<>();
        for (TaxonomyRelation r : relations) {
            dtos.add(relationService.toDto(r));
        }
        return dtos;
    }

    private void addRelationsFor(String code, List<TaxonomyRelationDto> result) {
        List<TaxonomyRelation> outgoing =
                relationRepository.findBySourceNodeCodeAndRelationTypeIn(code, WHITELISTED_TYPES);
        for (TaxonomyRelation r : outgoing) {
            result.add(relationService.toDto(r));
        }

        List<TaxonomyRelation> incoming =
                relationRepository.findByTargetNodeCodeAndRelationTypeIn(code, WHITELISTED_TYPES);
        for (TaxonomyRelation r : incoming) {
            if (r.isBidirectional()) {
                result.add(relationService.toDto(r));
            }
        }
    }
}