/**
* RDFElement.js - Base class for all Ragno RDF elements
*
* This abstract base class provides common functionality for all ragno:Element
* instances, including RDF triple management, SKOS properties, and provenance
* tracking following the ragno ontology specification.
*
* Key Features:
* - RDF-Ext integration for triple management
* - SKOS Concept compliance
* - Provenance tracking with PROV-O
* - URI management and validation
* - Common ragno properties implementation
*/
import rdf from 'rdf-ext'
import NamespaceManager from '../core/NamespaceManager.js'
import { logger } from '../../Utils.js'
export default class RDFElement {
/**
* Get the URI of this element
* @returns {string} The URI string
*/
getURI() {
return this.uri;
}
constructor(options = {}) {
// Initialize namespace manager
this.ns = new NamespaceManager(options)
// Create or use provided dataset
this.dataset = options.dataset || rdf.dataset()
// Generate or use provided URI
this.uri = options.uri || this.generateURI(options.type || 'element')
this.node = rdf.namedNode(this.uri)
// Track creation metadata
this.created = new Date()
this.modified = new Date()
// Initialize as ragno:Element and skos:Concept
this.addType(this.ns.classes.Element)
this.addType(this.ns.skos.Concept)
// Add creation timestamp
this.addTriple(
this.ns.dcProperties.created,
rdf.literal(this.created.toISOString(), this.ns.xsd.dateTime)
)
logger.debug(`Created RDFElement: ${this.uri}`)
}
/**
* Generate a unique URI for this element
* @param {string} type - Element type
* @returns {string} Generated URI
*/
generateURI(type) {
const timestamp = Date.now()
const random = Math.random().toString(36).substring(2, 8)
return `${this.ns.uriBase}${type}/${timestamp}-${random}`
}
/**
* Add a triple to the dataset with this element as subject
* @param {NamedNode} predicate - Predicate
* @param {NamedNode|Literal} object - Object
* @param {NamedNode} [graph] - Optional graph context
*/
addTriple(predicate, object, graph) {
const quad = graph
? rdf.quad(this.node, predicate, object, graph)
: rdf.quad(this.node, predicate, object)
this.dataset.add(quad)
this.updateModified()
}
/**
* Remove a triple from the dataset
* @param {NamedNode} predicate - Predicate
* @param {NamedNode|Literal} [object] - Object (optional for removing all)
*/
removeTriple(predicate, object = null) {
const toRemove = [...this.dataset.match(this.node, predicate, object)]
for (const quad of toRemove) {
this.dataset.delete(quad)
}
this.updateModified()
}
/**
* Add RDF type to this element
* @param {NamedNode} type - RDF type
*/
addType(type) {
this.addTriple(this.ns.rdf.type, type)
}
/**
* Set content for this element
* @param {string} content - Text content
*/
setContent(content) {
this.removeTriple(this.ns.properties.content)
this.addTriple(this.ns.properties.content, rdf.literal(content))
}
/**
* Get content of this element (or empty string if not set)
* @returns {string} Content text
*/
getContent() {
const quads = [...this.dataset.match(this.node, this.ns.properties.content)];
return quads.length > 0 && quads[0].object.value ? quads[0].object.value : '';
}
/**
* Set SKOS preferred label
* @param {string} label - Label text
* @param {string} [lang='en'] - Language tag
*/
setPrefLabel(label, lang = 'en') {
this.removeTriple(this.ns.skosProperties.prefLabel)
this.addTriple(this.ns.skosProperties.prefLabel, rdf.literal(label, lang))
}
/**
* Get SKOS preferred label
* @returns {string|null} Label text
*/
getPrefLabel() {
const quads = [...this.dataset.match(this.node, this.ns.skosProperties.prefLabel)]
return quads.length > 0 ? quads[0].object.value : null
}
/**
* Add SKOS alternative label
* @param {string} label - Alternative label
* @param {string} [lang='en'] - Language tag
*/
addAltLabel(label, lang = 'en') {
this.addTriple(this.ns.skosProperties.altLabel, rdf.literal(label, lang))
}
/**
* Set whether this element is an entry point
* @param {boolean} isEntryPoint - Entry point status
*/
setEntryPoint(isEntryPoint) {
this.removeTriple(this.ns.properties.isEntryPoint)
this.addTriple(this.ns.properties.isEntryPoint, rdf.literal(isEntryPoint))
}
/**
* Check if this element is an entry point
* @returns {boolean} Entry point status
*/
isEntryPoint() {
const quads = [...this.dataset.match(this.node, this.ns.properties.isEntryPoint)]
if (quads.length > 0) {
return quads[0].object.value === 'true'
}
return false
}
/**
* Set sub-type for this element
* @param {string} subType - Sub-type identifier
*/
setSubType(subType) {
this.removeTriple(this.ns.properties.subType)
this.addTriple(this.ns.properties.subType, this.ns.ex(subType))
}
/**
* Get sub-type of this element
* @returns {string|null} Sub-type identifier
*/
getSubType() {
const quads = [...this.dataset.match(this.node, this.ns.properties.subType)]
if (quads.length > 0) {
const uri = quads[0].object.value
return uri.substring(this.ns.uriBase.length)
}
return null
}
/**
* Connect this element to another element
* @param {RDFElement|NamedNode} target - Target element
* @param {number} [weight] - Optional weight
*/
connectTo(target, weight) {
const targetNode = target instanceof RDFElement ? target.node : target
this.addTriple(this.ns.properties.connectsTo, targetNode)
if (weight !== undefined) {
// Create reified statement for weight
const connection = rdf.namedNode(this.generateURI('connection'))
this.dataset.add(rdf.quad(connection, this.ns.rdf.subject, this.node))
this.dataset.add(rdf.quad(connection, this.ns.rdf.predicate, this.ns.properties.connectsTo))
this.dataset.add(rdf.quad(connection, this.ns.rdf.object, targetNode))
this.dataset.add(rdf.quad(connection, this.ns.properties.hasWeight, rdf.literal(weight)))
}
}
/**
* Add provenance information
* @param {string} sourceURI - Source document/entity URI
*/
addProvenance(sourceURI) {
this.addTriple(this.ns.provProperties.wasDerivedFrom, rdf.namedNode(sourceURI))
}
/**
* Set PPR score for this element
* @param {number} score - PPR score
*/
setPPRScore(score) {
this.removeTriple(this.ns.properties.hasPPRScore)
this.addTriple(this.ns.properties.hasPPRScore, rdf.literal(score))
}
/**
* Get PPR score for this element
* @returns {number|null} PPR score
*/
getPPRScore() {
const quads = [...this.dataset.match(this.node, this.ns.properties.hasPPRScore)]
return quads.length > 0 ? parseFloat(quads[0].object.value) : null
}
/**
* Set similarity score for this element
* @param {number} score - Similarity score
*/
setSimilarityScore(score) {
this.removeTriple(this.ns.properties.hasSimilarityScore)
this.addTriple(this.ns.properties.hasSimilarityScore, rdf.literal(score))
}
/**
* Get similarity score for this element
* @returns {number|null} Similarity score
*/
getSimilarityScore() {
const quads = [...this.dataset.match(this.node, this.ns.properties.hasSimilarityScore)]
return quads.length > 0 ? parseFloat(quads[0].object.value) : null
}
/**
* Get all triples where this element is the subject
* @returns {Array} Array of quads
*/
getTriples() {
return [...this.dataset.match(this.node)]
}
/**
* Get all triples with specific predicate
* @param {NamedNode} predicate - Predicate to match
* @returns {Array} Array of matching quads
*/
getTriplesWithPredicate(predicate) {
return [...this.dataset.match(this.node, predicate)]
}
/**
* Get all connected elements
* @returns {Array<NamedNode>} Array of connected element URIs
*/
getConnectedElements() {
return this.getTriplesWithPredicate(this.ns.properties.connectsTo)
.map(quad => quad.object)
}
/**
* Check if this element has a specific type
* @param {NamedNode} type - RDF type to check
* @returns {boolean} True if element has this type
*/
hasType(type) {
return this.getTriplesWithPredicate(this.ns.rdf.type)
.some(quad => quad.object.equals(type))
}
/**
* Get all types of this element
* @returns {Array<NamedNode>} Array of RDF types
*/
getTypes() {
return this.getTriplesWithPredicate(this.ns.rdf.type)
.map(quad => quad.object)
}
/**
* Update the modified timestamp
*/
updateModified() {
this.modified = new Date()
// Remove existing modified triples without triggering updateModified recursion
const toRemove = [...this.dataset.match(this.node, this.ns.dcProperties.modified)]
for (const quad of toRemove) {
this.dataset.delete(quad)
}
// Add new modified timestamp
const quad = rdf.quad(
this.node,
this.ns.dcProperties.modified,
rdf.literal(this.modified.toISOString(), this.ns.xsd.dateTime)
)
this.dataset.add(quad)
}
/**
* Validate this element against ragno ontology constraints
* @returns {Object} Validation result with errors array
*/
validate() {
const errors = []
// Check required types
if (!this.hasType(this.ns.classes.Element)) {
errors.push('Element must have ragno:Element type')
}
if (!this.hasType(this.ns.skos.Concept)) {
errors.push('Element must have skos:Concept type')
}
// Check URI format
if (!this.ns.validateRagnoURI || !this.ns.validateRagnoURI(this.uri, 'individual')) {
errors.push('Invalid URI format for ragno element')
}
return {
valid: errors.length === 0,
errors
}
}
/**
* Export this element as N-Triples
* @returns {string} N-Triples representation
*/
toNTriples() {
return this.getTriples().map(quad => {
const subject = `<${quad.subject.value}>`
const predicate = `<${quad.predicate.value}>`
const object = quad.object.termType === 'Literal'
? `"${quad.object.value}"${quad.object.language ? `@${quad.object.language}` : ''}${quad.object.datatype ? `^^<${quad.object.datatype.value}>` : ''}`
: `<${quad.object.value}>`
return `${subject} ${predicate} ${object} .`
}).join('\n')
}
/**
* Export this element as Turtle (simplified)
* @returns {string} Turtle representation
*/
toTurtle() {
const prefixes = this.ns.getTurtlePrefixes()
const compressedURI = this.ns.compress(this.uri)
const triples = this.getTriples().map(quad => {
const predicate = this.ns.compress(quad.predicate.value)
const object = quad.object.termType === 'Literal'
? `"${quad.object.value}"${quad.object.language ? `@${quad.object.language}` : ''}${quad.object.datatype ? `^^${this.ns.compress(quad.object.datatype.value)}` : ''}`
: this.ns.compress(quad.object.value)
return ` ${predicate} ${object}`
}).join(' ;\n')
return `${prefixes}\n\n${compressedURI}\n${triples} .`
}
/**
* Set a metadata property as an RDF triple
* @param {string} property - Property name
* @param {any} value - Property value
*/
setMetadataProperty(property, value) {
const predicate = this.ns.properties[property] || this.ns.ex(property)
this.removeTriple(predicate)
// Handle different value types
let object
if (typeof value === 'boolean') {
object = rdf.literal(value.toString(), this.ns.xsd.boolean)
} else if (typeof value === 'number') {
if (Number.isInteger(value)) {
object = rdf.literal(value.toString(), this.ns.xsd.integer)
} else {
object = rdf.literal(value.toString(), this.ns.xsd.double)
}
} else if (value instanceof Date) {
object = rdf.literal(value.toISOString(), this.ns.xsd.dateTime)
} else if (typeof value === 'string' && value.startsWith('http')) {
// Assume URIs should be stored as named nodes
object = rdf.namedNode(value)
} else {
// Default to string literal
object = rdf.literal(value.toString())
}
this.addTriple(predicate, object)
}
/**
* Get a metadata property value
* @param {string} property - Property name
* @returns {any} Property value or undefined
*/
getMetadataProperty(property) {
const predicate = this.ns.properties[property] || this.ns.ex(property)
const quads = [...this.dataset.match(this.node, predicate)]
if (quads.length === 0) {
return undefined
}
const object = quads[0].object
// Convert back to appropriate JavaScript type
if (object.termType === 'Literal') {
if (object.datatype) {
const datatypeURI = object.datatype.value
if (datatypeURI === this.ns.xsd.boolean.value) {
return object.value === 'true'
} else if (datatypeURI === this.ns.xsd.integer.value) {
return parseInt(object.value)
} else if (datatypeURI === this.ns.xsd.double.value || datatypeURI === this.ns.xsd.float.value) {
return parseFloat(object.value)
} else if (datatypeURI === this.ns.xsd.dateTime.value) {
return new Date(object.value)
}
}
return object.value
} else if (object.termType === 'NamedNode') {
return object.value
}
return object.value
}
/**
* Get all custom metadata properties (excluding standard properties)
* @returns {Object} Object with all custom metadata
*/
getAllCustomMetadata() {
const metadata = {}
const standardPredicates = new Set([
this.ns.rdf.type.value,
this.ns.properties.content.value,
this.ns.skosProperties.prefLabel.value,
this.ns.skosProperties.altLabel.value,
this.ns.properties.isEntryPoint.value,
this.ns.properties.subType.value,
this.ns.dcProperties.created.value,
this.ns.dcProperties.modified.value,
this.ns.properties.connectsTo.value,
this.ns.properties.hasPPRScore.value,
this.ns.properties.hasSimilarityScore.value
])
for (const quad of this.getTriples()) {
const predicateURI = quad.predicate.value
// Skip standard properties
if (standardPredicates.has(predicateURI)) {
continue
}
// Extract property name from URI
let propertyName
if (predicateURI.startsWith(this.ns.uriBase)) {
propertyName = predicateURI.substring(this.ns.uriBase.length)
} else {
propertyName = predicateURI.split('/').pop() || predicateURI.split('#').pop()
}
// Convert value
const object = quad.object
let value
if (object.termType === 'Literal') {
if (object.datatype) {
const datatypeURI = object.datatype.value
if (datatypeURI === this.ns.xsd.boolean.value) {
value = object.value === 'true'
} else if (datatypeURI === this.ns.xsd.integer.value) {
value = parseInt(object.value)
} else if (datatypeURI === this.ns.xsd.double.value || datatypeURI === this.ns.xsd.float.value) {
value = parseFloat(object.value)
} else if (datatypeURI === this.ns.xsd.dateTime.value) {
value = new Date(object.value)
} else {
value = object.value
}
} else {
value = object.value
}
} else if (object.termType === 'NamedNode') {
value = object.value
} else {
value = object.value
}
metadata[propertyName] = value
}
return metadata
}
/**
* Set multiple metadata properties at once
* @param {Object} metadata - Object with property/value pairs
*/
setAllMetadata(metadata) {
if (!metadata || typeof metadata !== 'object') {
return
}
for (const [property, value] of Object.entries(metadata)) {
this.setMetadataProperty(property, value)
}
}
/**
* Get element metadata (enhanced to include custom metadata)
* @returns {Object} Element metadata including custom properties
*/
getMetadata() {
const standardMetadata = {
uri: this.uri,
types: this.getTypes().map(type => this.ns.compress(type.value)),
prefLabel: this.getPrefLabel(),
content: this.getContent(),
isEntryPoint: this.isEntryPoint(),
subType: this.getSubType(),
created: this.created,
modified: this.modified,
tripleCount: this.getTriples().length,
connections: this.getConnectedElements().length
}
// Merge with custom metadata
const customMetadata = this.getAllCustomMetadata()
return { ...standardMetadata, ...customMetadata }
}
/**
* Clone this element with a new URI
* @param {Object} [options] - Options for the cloned element
* @returns {RDFElement} Cloned element
*/
clone(options = {}) {
const cloned = new RDFElement({
...options,
type: options.type || 'element'
})
// Copy all triples except URI-specific ones
for (const quad of this.getTriples()) {
if (!quad.predicate.equals(this.ns.dcProperties.created)) {
cloned.addTriple(quad.predicate, quad.object)
}
}
return cloned
}
/**
* Export all triples of this element to a target dataset
* @param {Dataset} targetDataset - The RDF-Ext dataset to export to
*/
exportToDataset(targetDataset) {
for (const quad of this.getTriples()) {
targetDataset.add(quad)
}
}
}