Source: ragno/core/RDFGraphManager.js

/**
 * RDFGraphManager.js - Core RDF-Ext integration for Ragno
 * 
 * This class provides the foundation for RDF-based graph operations in Ragno,
 * replacing the simple object-oriented approach with proper RDF semantics
 * following the ragno ontology specification.
 * 
 * Key Features:
 * - RDF-Ext dataset management
 * - Namespace resolution for ragno ontology
 * - SPARQL integration with existing SPARQLStore
 * - Graph serialization/deserialization
 * - URI generation for graph elements
 */

import rdf from 'rdf-ext'
import namespace from '@rdfjs/namespace'
import { logger } from '../../Utils.js'

export default class RDFGraphManager {
    constructor(options = {}) {
        this.dataset = rdf.dataset()
        this.uriBase = options.uriBase || 'http://example.org/ragno/'
        this.counters = new Map() // For URI generation
        
        // Initialize namespaces following ragno ontology
        this.ns = {
            rdf: namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#'),
            rdfs: namespace('http://www.w3.org/2000/01/rdf-schema#'),
            owl: namespace('http://www.w3.org/2002/07/owl#'),
            xsd: namespace('http://www.w3.org/2001/XMLSchema#'),
            skos: namespace('http://www.w3.org/2004/02/skos/core#'),
            prov: namespace('http://www.w3.org/ns/prov#'),
            dcterms: namespace('http://purl.org/dc/terms/'),
            ragno: namespace('http://purl.org/stuff/ragno/'),
            ex: namespace(this.uriBase)
        }
        
        logger.info('RDFGraphManager initialized with RDF-Ext dataset')
    }

    /**
     * Generate a unique URI for a given resource type
     * @param {string} type - Resource type (entity, unit, relationship, etc.)
     * @returns {string} Generated URI
     */
    generateURI(type) {
        const count = this.counters.get(type) || 0
        this.counters.set(type, count + 1)
        return `${this.uriBase}${type}/${count + 1}`
    }

    /**
     * Create a new named node with generated URI
     * @param {string} type - Resource type
     * @returns {NamedNode} RDF named node
     */
    createNamedNode(type) {
        const uri = this.generateURI(type)
        return rdf.namedNode(uri)
    }

    /**
     * Add a triple to the dataset
     * @param {NamedNode} subject - Subject node
     * @param {NamedNode} predicate - Predicate node  
     * @param {NamedNode|Literal} object - Object node or literal
     * @param {NamedNode} [graph] - Optional graph context
     */
    addTriple(subject, predicate, object, graph) {
        const quad = graph 
            ? rdf.quad(subject, predicate, object, graph)
            : rdf.quad(subject, predicate, object)
        this.dataset.add(quad)
    }

    /**
     * Add type declaration for a resource
     * @param {NamedNode} subject - Resource to type
     * @param {NamedNode} type - RDF type (e.g., ragno:Entity)
     */
    addType(subject, type) {
        this.addTriple(subject, this.ns.rdf.type, type)
    }

    /**
     * Add content property to a resource
     * @param {NamedNode} subject - Resource
     * @param {string} content - Text content
     */
    addContent(subject, content) {
        this.addTriple(subject, this.ns.ragno.content, rdf.literal(content))
    }

    /**
     * Add SKOS preferred label
     * @param {NamedNode} subject - Resource
     * @param {string} label - Label text
     * @param {string} [lang='en'] - Language tag
     */
    addLabel(subject, label, lang = 'en') {
        this.addTriple(subject, this.ns.skos.prefLabel, rdf.literal(label, lang))
    }

    /**
     * Create a ragno:Entity with proper RDF structure
     * @param {string} name - Entity name
     * @param {boolean} [isEntryPoint=true] - Whether entity is an entry point
     * @returns {NamedNode} Created entity node
     */
    createEntity(name, isEntryPoint = true) {
        const entity = this.createNamedNode('entity')
        
        // Add required RDF properties
        this.addType(entity, this.ns.ragno.Entity)
        this.addLabel(entity, name)
        this.addTriple(entity, this.ns.ragno.isEntryPoint, rdf.literal(isEntryPoint))
        
        logger.debug(`Created ragno:Entity: ${entity.value} with name "${name}"`)
        return entity
    }

    /**
     * Create a ragno:Unit with proper RDF structure
     * @param {string} content - Unit content
     * @param {string} [source] - Source document reference
     * @returns {NamedNode} Created unit node
     */
    createUnit(content, source) {
        const unit = this.createNamedNode('unit')
        
        // Add required RDF properties
        this.addType(unit, this.ns.ragno.Unit)
        this.addContent(unit, content)
        
        if (source) {
            this.addTriple(unit, this.ns.ragno.hasSourceDocument, rdf.namedNode(source))
        }
        
        logger.debug(`Created ragno:Unit: ${unit.value}`)
        return unit
    }

    /**
     * Create a ragno:Relationship as first-class RDF resource
     * @param {NamedNode} sourceEntity - Source entity
     * @param {NamedNode} targetEntity - Target entity
     * @param {string} description - Relationship description
     * @param {number} [weight=1.0] - Relationship weight
     * @returns {NamedNode} Created relationship node
     */
    createRelationship(sourceEntity, targetEntity, description, weight = 1.0) {
        const relationship = this.createNamedNode('relationship')
        
        // Add required RDF properties following ragno ontology
        this.addType(relationship, this.ns.ragno.Relationship)
        this.addTriple(relationship, this.ns.ragno.hasSourceEntity, sourceEntity)
        this.addTriple(relationship, this.ns.ragno.hasTargetEntity, targetEntity)
        this.addContent(relationship, description)
        this.addTriple(relationship, this.ns.ragno.hasWeight, rdf.literal(weight))
        
        logger.debug(`Created ragno:Relationship: ${relationship.value} between ${sourceEntity.value} and ${targetEntity.value}`)
        return relationship
    }

    /**
     * Create a ragno:Attribute with entity connection
     * @param {NamedNode} entity - Entity this attribute describes
     * @param {string} content - Attribute content
     * @param {string} [subType] - Attribute subtype (e.g., "Overview")
     * @returns {NamedNode} Created attribute node
     */
    createAttribute(entity, content, subType) {
        const attribute = this.createNamedNode('attribute')
        
        // Add required RDF properties
        this.addType(attribute, this.ns.ragno.Attribute)
        this.addContent(attribute, content)
        
        if (subType) {
            this.addTriple(attribute, this.ns.ragno.subType, this.ns.ex(subType))
        }
        
        // Link to entity
        this.addTriple(entity, this.ns.ragno.hasAttribute, attribute)
        
        logger.debug(`Created ragno:Attribute: ${attribute.value} for entity ${entity.value}`)
        return attribute
    }

    /**
     * Connect two elements with ragno:connectsTo
     * @param {NamedNode} source - Source element
     * @param {NamedNode} target - Target element
     * @param {number} [weight] - Optional connection weight
     */
    connectElements(source, target, weight) {
        this.addTriple(source, this.ns.ragno.connectsTo, target)
        
        if (weight !== undefined) {
            // Create a reified statement for the weight
            const connection = this.createNamedNode('connection')
            this.addTriple(connection, this.ns.rdf.subject, source)
            this.addTriple(connection, this.ns.rdf.predicate, this.ns.ragno.connectsTo)
            this.addTriple(connection, this.ns.rdf.object, target)
            this.addTriple(connection, this.ns.ragno.hasWeight, rdf.literal(weight))
        }
    }

    /**
     * Query the dataset using SPARQL-like patterns
     * @param {NamedNode} [subject] - Subject pattern (null for any)
     * @param {NamedNode} [predicate] - Predicate pattern (null for any)
     * @param {NamedNode|Literal} [object] - Object pattern (null for any)
     * @returns {Array} Array of matching quads
     */
    query(subject = null, predicate = null, object = null) {
        return [...this.dataset.match(subject, predicate, object)]
    }

    /**
     * Get all entities in the dataset
     * @returns {Array<NamedNode>} Array of entity nodes
     */
    getEntities() {
        return this.query(null, this.ns.rdf.type, this.ns.ragno.Entity)
            .map(quad => quad.subject)
    }

    /**
     * Get all relationships in the dataset
     * @returns {Array<NamedNode>} Array of relationship nodes
     */
    getRelationships() {
        return this.query(null, this.ns.rdf.type, this.ns.ragno.Relationship)
            .map(quad => quad.subject)
    }

    /**
     * Get all units in the dataset
     * @returns {Array<NamedNode>} Array of unit nodes
     */
    getUnits() {
        return this.query(null, this.ns.rdf.type, this.ns.ragno.Unit)
            .map(quad => quad.subject)
    }

    /**
     * Export dataset to N-Triples format
     * @returns {string} N-Triples serialization
     */
    toNTriples() {
        return [...this.dataset].map(quad => {
            const subject = quad.subject.value
            const predicate = quad.predicate.value
            const object = quad.object.termType === 'Literal' 
                ? `"${quad.object.value}"` 
                : quad.object.value
            return `<${subject}> <${predicate}> <${object}> .`
        }).join('\n')
    }

    /**
     * Get dataset statistics
     * @returns {Object} Statistics about the graph
     */
    getStats() {
        const entities = this.getEntities().length
        const relationships = this.getRelationships().length
        const units = this.getUnits().length
        const totalTriples = this.dataset.size
        
        return {
            entities,
            relationships,
            units,
            totalTriples,
            types: {
                'ragno:Entity': entities,
                'ragno:Relationship': relationships,
                'ragno:Unit': units
            }
        }
    }

    /**
     * Clear the dataset
     */
    clear() {
        this.dataset = rdf.dataset()
        this.counters.clear()
        logger.info('RDF dataset cleared')
    }

    /**
     * Clone the current dataset
     * @returns {RDFGraphManager} New instance with cloned data
     */
    clone() {
        const cloned = new RDFGraphManager({ uriBase: this.uriBase })
        for (const quad of this.dataset) {
            cloned.dataset.add(quad)
        }
        cloned.counters = new Map(this.counters)
        return cloned
    }
}