RDF Integration with Side-Channels
This tutorial covers advanced RDF integration using Kastor Gen’s side-channel architecture to access RDF-specific functionality when needed.
Understanding the Side-Channel
The side-channel provides access to RDF functionality without polluting your domain interfaces:
// Pure domain interface
interface Person {
val name: List<String>
val age: List<Int>
}
// RDF access via side-channel
val person: Person = graph.materialize(node)
val rdfHandle = person.asRdf() // Side-channel access
RdfHandle Interface
The RdfHandle provides access to RDF-specific functionality:
interface RdfHandle {
val node: RdfTerm // The RDF node (IRI or blank node)
val graph: RdfGraph // The containing RDF graph
val extras: PropertyBag // Unmapped properties
fun validate(): ValidationResult
fun validateOrThrow() // SHACL validation
}
Accessing the Underlying RDF Node
val person: Person = graph.materialize(node)
val rdfHandle = person.asRdf()
val repo = Rdf.memory()
// Get the RDF node
val node = rdfHandle.node
when (node) {
is Iri -> println("Person IRI: ${node.value}")
is BlankNode -> println("Person blank node: ${node.id}")
}
// Get the containing graph
val graph = rdfHandle.graph
println("Graph contains ${graph.getTriples().size} triples")
Direct RDF Operations
val person: Person = graph.materialize(node)
val rdfHandle = person.asRdf()
// Add new triples to the graph
val graph = rdfHandle.graph
graph.add {
val node = rdfHandle.node
node - DCTERMS.modified - "2024-01-15"^^XSD.date
node - SKOS.note - "Updated via side-channel"
}
// Query the graph directly
val triples = graph.getTriples()
.filter { it.subject == rdfHandle.node }
.filter { it.predicate == FOAF.name }
println("Name triples: ${triples.size}")
PropertyBag for Unmapped Properties
The PropertyBag provides typed access to properties not mapped in your domain interface:
interface PropertyBag {
fun predicates(): Set<Iri> // All unmapped predicates
fun values(pred: Iri): List<RdfTerm> // All values for a predicate
fun literals(pred: Iri): List<Literal> // Literal values only
fun strings(pred: Iri): List<String> // String values
fun iris(pred: Iri): List<Iri> // IRI values
fun <T : Any> objects(pred: Iri, asType: Class<T>): List<T> // Materialized objects
}
Discovering Unmapped Properties
val person: Person = graph.materialize(node)
val extras = person.asRdf().extras
// Get all unmapped predicates
val unmappedPredicates = extras.predicates()
println("Unmapped properties:")
unmappedPredicates.forEach { pred ->
println(" ${pred.value}")
}
// Check if specific properties exist
if (extras.predicates().contains(SKOS.altLabel)) {
println("Person has alternative labels")
}
Accessing Unmapped Values
val person: Person = graph.materialize(node)
val extras = person.asRdf().extras
// Get string values
val altLabels = extras.strings(SKOS.altLabel)
println("Alternative labels: ${altLabels.joinToString(", ")}")
// Get literal values
val descriptions = extras.literals(DCTERMS.description)
descriptions.forEach { literal ->
println("Description: ${literal.lexical}")
if (literal is LangString) {
println("Language: ${literal.lang}")
}
}
// Get IRI values
val relatedIris = extras.iris(DCTERMS.relation)
relatedIris.forEach { iri ->
println("Related: ${iri.value}")
}
Materializing Unmapped Objects
val person: Person = graph.materialize(node)
val extras = person.asRdf().extras
// Materialize related objects
val relatedPeople = extras.objects(FOAF.knows, Person::class.java)
relatedPeople.forEach { relatedPerson ->
println("Related person: ${relatedPerson.name.firstOrNull()}")
}
// Materialize different types
val organizations = extras.objects(FOAF.member, Organization::class.java)
val events = extras.objects(FOAF.attended, Event::class.java)
Advanced RDF Operations
Custom RDF Queries
val person: Person = graph.materialize(node)
val rdfHandle = person.asRdf()
// Custom SPARQL query
val query = """
SELECT ?predicate ?object WHERE {
<${rdfHandle.node}> ?predicate ?object .
FILTER(?predicate != <http://xmlns.com/foaf/0.1/name>)
}
""".trimIndent()
val results = repo.select(SparqlSelectQuery(query))
results.forEach { bindingSet ->
val predicate = bindingSet.getValue("predicate") as Iri
val obj = bindingSet.getValue("object")
println("${predicate.value}: ${obj}")
}
RDF Graph Manipulation
val person: Person = graph.materialize(node)
val rdfHandle = person.asRdf()
// Add new properties
rdfHandle.graph.add {
val node = rdfHandle.node
node - DCTERMS.created - "2024-01-15"^^XSD.date
node - SKOS.note - "Created via Kastor Gen"
}
// Remove properties
val triplesToRemove = rdfHandle.graph.getTriples()
.filter { it.subject == rdfHandle.node }
.filter { it.predicate == SKOS.note }
triplesToRemove.forEach { triple ->
rdfHandle.graph.remove(triple)
}
// Update properties
rdfHandle.graph.add {
val node = rdfHandle.node
node - DCTERMS.modified - "2024-01-16"^^XSD.date
}
Working with Multiple Graphs
val person: Person = graph.materialize(node)
val rdfHandle = person.asRdf()
// Get the default graph
val defaultGraph = rdfHandle.graph
// Access named graphs if available
val repository = defaultGraph.repository
val namedGraphs = repository.listGraphs()
namedGraphs.forEach { graphName ->
val namedGraph = repository.getGraph(graphName)
val personInNamedGraph = namedGraph.getTriples()
.filter { it.subject == rdfHandle.node }
.firstOrNull()
if (personInNamedGraph != null) {
println("Person found in named graph: ${graphName}")
}
}
Validation Integration
SHACL Validation
val validation = JenaValidation.fromTurtle(shapesTtl) // shapesTtl: your SHACL shapes as Turtle text
val person: Person = rdfRef.asValidatedType(validation)
val rdfHandle = person.asRdf()
// Validate against SHACL shapes
try {
rdfHandle.validateOrThrow()
println("Validation passed")
} catch (e: ValidationException) {
println("Validation failed: ${e.message}")
}
// Materialize with validation
val validatedPerson: Person = rdfRef.asValidatedType(validation)
Custom Validation
val person: Person = graph.materialize(node)
val rdfHandle = person.asRdf()
// Custom validation logic
fun validatePerson(person: Person): List<String> {
val errors = mutableListOf<String>()
val rdfHandle = person.asRdf()
// Check required properties
if (person.name.isEmpty()) {
errors.add("Person must have a name")
}
// Check RDF-specific constraints
val age = person.age.firstOrNull()
if (age != null && age < 0) {
errors.add("Age cannot be negative")
}
// Check for required RDF properties
val extras = rdfHandle.extras
if (!extras.predicates().contains(DCTERMS.created)) {
errors.add("Person must have a creation date")
}
return errors
}
// Usage
val validationErrors = validatePerson(person)
if (validationErrors.isNotEmpty()) {
println("Validation errors:")
validationErrors.forEach { error ->
println(" - $error")
}
}
Performance Optimization
Lazy Evaluation
val person: Person = graph.materialize(node)
// Properties are evaluated lazily
println("Before accessing name") // No RDF queries yet
val name = person.name.firstOrNull() // Now RDF query is executed
println("After accessing name")
// Results are cached
val name2 = person.name.firstOrNull() // Uses cached result
Batch Operations
// e.g. subjectNodes from a SPARQL result or graph.walk over (s a foaf:Person)
val people: List<Person> = subjectNodes.materializeIn<Person>(graph)
// Process in batches to avoid memory issues
people.chunked(100).forEach { batch ->
batch.forEach { person ->
val rdfHandle = person.asRdf()
val extras = rdfHandle.extras
// Process unmapped properties
val unmappedProps = extras.predicates()
// ... process properties
}
}
Selective Property Access
val person: Person = graph.materialize(node)
// Only access needed properties
val name = person.name.firstOrNull()
val age = person.age.firstOrNull()
// Access side-channel only when needed
if (needsRdfOperations) {
val rdfHandle = person.asRdf()
val extras = rdfHandle.extras
// ... RDF operations
}
Integration Patterns
Repository Pattern
class PersonRepository(private val repository: RdfRepository) {
fun findById(id: Iri): Person? {
val graph = repository.defaultGraph
val personRef = RdfRef(id, graph)
return try {
personRef.asType<Person>()
} catch (e: IllegalStateException) {
null
}
}
fun findAll(): List<Person> {
val graph = repository.defaultGraph
val personIris = graph.getTriples()
.filter { it.predicate == RDF.type && it.obj == FOAF.Person }
.map { it.subject as Iri }
return personIris.mapNotNull { iri ->
try {
RdfRef(iri, graph).asType<Person>()
} catch (e: IllegalStateException) {
null
}
}
}
fun save(person: Person) {
val rdfHandle = person.asRdf()
val graph = rdfHandle.graph
// Add or update triples
graph.add {
val node = rdfHandle.node
person.name.forEach { name ->
node - FOAF.name - name
}
person.age.forEach { age ->
node - FOAF.age - age
}
}
}
}
Service Layer Pattern
class PersonService(private val repository: PersonRepository) {
fun createPerson(name: String, age: Int): Person {
val repo = Rdf.memory()
val personId = iri("http://example.org/person/${UUID.randomUUID()}")
repo.add {
personId - RDF.type - FOAF.Person
personId - FOAF.name - name
personId - FOAF.age - age
personId - DCTERMS.created - "2024-01-15"^^XSD.date
}
return RdfRef(personId, repo.defaultGraph).asType()
}
fun updatePerson(person: Person, updates: Map<String, Any>) {
val rdfHandle = person.asRdf()
val graph = rdfHandle.graph
graph.add {
val node = rdfHandle.node
updates.forEach { (key, value) ->
when (key) {
"name" -> node - FOAF.name - value.toString()
"age" -> node - FOAF.age - (value as Int)
"email" -> node - FOAF.mbox - value.toString()
}
}
node - DCTERMS.modified - "2024-01-16"^^XSD.date
}
}
fun getPersonWithMetadata(person: Person): PersonWithMetadata {
val rdfHandle = person.asRdf()
val extras = rdfHandle.extras
return PersonWithMetadata(
person = person,
created = extras.strings(DCTERMS.created).firstOrNull(),
modified = extras.strings(DCTERMS.modified).firstOrNull(),
notes = extras.strings(SKOS.note),
tags = extras.strings(SKOS.altLabel)
)
}
}
data class PersonWithMetadata(
val person: Person,
val created: String?,
val modified: String?,
val notes: List<String>,
val tags: List<String>
)
Error Handling
Graceful Degradation
fun processPerson(person: Person): ProcessedPerson {
val rdfHandle = person.asRdf()
val extras = rdfHandle.extras
return ProcessedPerson(
name = person.name.firstOrNull() ?: "Unknown",
age = person.age.firstOrNull() ?: 0,
email = person.email.firstOrNull(),
metadata = try {
PersonMetadata(
created = extras.strings(DCTERMS.created).firstOrNull(),
notes = extras.strings(SKOS.note)
)
} catch (e: Exception) {
PersonMetadata() // Default metadata
}
)
}
Validation Error Handling
fun validateAndProcess(person: Person): ProcessingResult {
return try {
// Try validation first
person.asRdf().validateOrThrow()
// Process if validation passes
val processed = processPerson(person)
ProcessingResult.Success(processed)
} catch (e: ValidationException) {
ProcessingResult.ValidationError(e.message ?: "Validation failed")
} catch (e: Exception) {
ProcessingResult.ProcessingError(e.message ?: "Processing failed")
}
}
sealed class ProcessingResult {
data class Success(val person: ProcessedPerson) : ProcessingResult()
data class ValidationError(val message: String) : ProcessingResult()
data class ProcessingError(val message: String) : ProcessingResult()
}
Best Practices
✅ Do
- Use side-channel only when needed
- Cache expensive RDF operations
- Handle validation errors gracefully
- Use typed access methods (
strings(),iris(), etc.) - Implement proper error handling
- Consider performance implications
- Use batch operations for large datasets
❌ Don’t
- Access side-channel in tight loops
- Ignore validation errors
- Mix RDF operations with domain logic
- Assume properties always exist
- Perform expensive operations synchronously
- Ignore memory usage with large graphs
Next Steps
- Learn about Validation - SHACL validation patterns
- Check out Advanced Usage - Complex scenarios
- See Practical Examples - Real-world use cases
- Review API Reference - Complete API documentation