TaxonomyNode.java

package com.taxonomy.catalog.model;

import com.taxonomy.search.NodeEmbeddingBinder;
import jakarta.persistence.*;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Nationalized;
import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.TypeBinderRef;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*;

import java.util.ArrayList;
import java.util.List;
import com.taxonomy.shared.model.FloatArrayConverter;

@Entity
@Table(name = "taxonomy_node")
@Indexed
@TypeBinding(binder = @TypeBinderRef(type = NodeEmbeddingBinder.class))
public class TaxonomyNode {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Nationalized
    @Column(unique = true, nullable = false)
    @KeywordField
    private String code;

    @Nationalized
    @Column(nullable = true)
    @KeywordField
    private String uuid;

    @Nationalized
    @Column(nullable = false)
    @FullTextField(analyzer = "english")
    private String nameEn;

    @Nationalized
    @Column(nullable = true)
    @FullTextField(analyzer = "german")
    private String nameDe;

    @Nationalized
    @Lob
    @Column(length = 5000)
    @FullTextField(analyzer = "english")
    private String descriptionEn;

    @Nationalized
    @Lob
    @Column(length = 5000)
    @FullTextField(analyzer = "german")
    private String descriptionDe;

    @Nationalized
    @Column(name = "parent_code")
    @GenericField
    private String parentCode;

    @Nationalized
    @Column(name = "taxonomy_root")
    @KeywordField
    private String taxonomyRoot;

    @GenericField
    @Column(name = "node_level")
    private int level;

    @Nationalized
    private String dataset;

    @Nationalized
    @Column(name = "external_id")
    @KeywordField
    private String externalId;

    @Nationalized
    private String source;

    @Nationalized
    @Lob
    @Column(length = 5000)
    private String reference;

    @Column(name = "sort_order")
    private Integer sortOrder;

    @Nationalized
    private String state;

    @Lob
    @Convert(converter = FloatArrayConverter.class)
    @Column(name = "semantic_embedding")
    private float[] semanticEmbedding;

    @GenericField
    @Column(name = "has_embedding")
    private boolean hasEmbedding;

    // ── Derived graph metadata ───────────────────────────────────────

    @GenericField(sortable = org.hibernate.search.engine.backend.types.Sortable.YES)
    @Column(name = "incoming_relation_count")
    private int incomingRelationCount;

    @GenericField(sortable = org.hibernate.search.engine.backend.types.Sortable.YES)
    @Column(name = "outgoing_relation_count")
    private int outgoingRelationCount;

    @GenericField(sortable = org.hibernate.search.engine.backend.types.Sortable.YES)
    @Column(name = "requirement_coverage_count")
    private int requirementCoverageCount;

    @KeywordField
    @Column(name = "graph_role")
    private String graphRole;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private TaxonomyNode parent;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
    @OrderBy("nameEn ASC")
    @BatchSize(size = 50)
    private List<TaxonomyNode> children = new ArrayList<>();

    @OneToMany(mappedBy = "sourceNode", cascade = CascadeType.ALL, orphanRemoval = true)
    @BatchSize(size = 50)
    private List<TaxonomyRelation> outgoingRelations = new ArrayList<>();

    @OneToMany(mappedBy = "targetNode", cascade = CascadeType.ALL, orphanRemoval = true)
    @BatchSize(size = 50)
    private List<TaxonomyRelation> incomingRelations = new ArrayList<>();

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getCode() { return code; }
    public void setCode(String code) { this.code = code; }

    public String getUuid() { return uuid; }
    public void setUuid(String uuid) { this.uuid = uuid; }

    public String getNameEn() { return nameEn; }
    public void setNameEn(String nameEn) { this.nameEn = nameEn; }

    public String getNameDe() { return nameDe; }
    public void setNameDe(String nameDe) { this.nameDe = nameDe; }

    /** Backward-compatible getter; returns the English name. */
    public String getName() { return nameEn; }

    public String getDescriptionEn() { return descriptionEn; }
    public void setDescriptionEn(String descriptionEn) { this.descriptionEn = descriptionEn; }

    public String getDescriptionDe() { return descriptionDe; }
    public void setDescriptionDe(String descriptionDe) { this.descriptionDe = descriptionDe; }

    /** Backward-compatible getter; returns the English description. */
    public String getDescription() { return descriptionEn; }

    public String getParentCode() { return parentCode; }
    public void setParentCode(String parentCode) { this.parentCode = parentCode; }

    public String getTaxonomyRoot() { return taxonomyRoot; }
    public void setTaxonomyRoot(String taxonomyRoot) { this.taxonomyRoot = taxonomyRoot; }

    public int getLevel() { return level; }
    public void setLevel(int level) { this.level = level; }

    public String getDataset() { return dataset; }
    public void setDataset(String dataset) { this.dataset = dataset; }

    public String getExternalId() { return externalId; }
    public void setExternalId(String externalId) { this.externalId = externalId; }

    public String getSource() { return source; }
    public void setSource(String source) { this.source = source; }

    public String getReference() { return reference; }
    public void setReference(String reference) { this.reference = reference; }

    public Integer getSortOrder() { return sortOrder; }
    public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }

    public String getState() { return state; }
    public void setState(String state) { this.state = state; }

    public float[] getSemanticEmbedding() { return semanticEmbedding; }
    public void setSemanticEmbedding(float[] semanticEmbedding) {
        this.semanticEmbedding = semanticEmbedding;
        this.hasEmbedding = (semanticEmbedding != null);
    }

    public boolean isHasEmbedding() { return hasEmbedding; }
    private void setHasEmbedding(boolean hasEmbedding) { this.hasEmbedding = hasEmbedding; }

    public int getIncomingRelationCount() { return incomingRelationCount; }
    public void setIncomingRelationCount(int incomingRelationCount) { this.incomingRelationCount = incomingRelationCount; }

    public int getOutgoingRelationCount() { return outgoingRelationCount; }
    public void setOutgoingRelationCount(int outgoingRelationCount) { this.outgoingRelationCount = outgoingRelationCount; }

    public int getRequirementCoverageCount() { return requirementCoverageCount; }
    public void setRequirementCoverageCount(int requirementCoverageCount) { this.requirementCoverageCount = requirementCoverageCount; }

    public String getGraphRole() { return graphRole; }
    public void setGraphRole(String graphRole) { this.graphRole = graphRole; }

    /** Total number of relations (incoming + outgoing). */
    public int getTotalRelationCount() { return incomingRelationCount + outgoingRelationCount; }

    public TaxonomyNode getParent() { return parent; }
    public void setParent(TaxonomyNode parent) { this.parent = parent; }

    public List<TaxonomyNode> getChildren() { return children; }
    public void setChildren(List<TaxonomyNode> children) { this.children = children; }

    public List<TaxonomyRelation> getOutgoingRelations() { return outgoingRelations; }
    public void setOutgoingRelations(List<TaxonomyRelation> outgoingRelations) { this.outgoingRelations = outgoingRelations; }

    public List<TaxonomyRelation> getIncomingRelations() { return incomingRelations; }
    public void setIncomingRelations(List<TaxonomyRelation> incomingRelations) { this.incomingRelations = incomingRelations; }
}