001/* 002 * SPDX-License-Identifier: Apache-2.0 003 * 004 * Copyright 2024-2026 The Enola <https://enola.dev> Authors 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package dev.enola.rdf.proto; 019 020import static java.util.Collections.singleton; 021 022import com.google.common.base.Strings; 023import com.google.common.collect.ImmutableList; 024 025import dev.enola.common.convert.ConversionException; 026import dev.enola.common.convert.ConverterInto; 027import dev.enola.thing.proto.Thing; 028import dev.enola.thing.proto.ThingOrBuilder; 029import dev.enola.thing.proto.Value.LangString; 030 031import org.eclipse.rdf4j.model.BNode; 032import org.eclipse.rdf4j.model.IRI; 033import org.eclipse.rdf4j.model.Model; 034import org.eclipse.rdf4j.model.Resource; 035import org.eclipse.rdf4j.model.Statement; 036import org.eclipse.rdf4j.model.ValueFactory; 037import org.eclipse.rdf4j.model.impl.SimpleValueFactory; 038import org.eclipse.rdf4j.model.impl.ValidatingValueFactory; 039import org.eclipse.rdf4j.rio.RDFHandler; 040 041import java.util.HashMap; 042import java.util.Map; 043 044/** 045 * Converts a Proto {@link Thing} to an RDF4j {@link Model} or into an RDF4j {@link 046 * org.eclipse.rdf4j.rio.RDFHandler}. 047 * 048 * <p>See {@link RdfProtoThingsConverter} for the "opposite" of this. 049 */ 050public class ProtoThingRdfConverter 051 implements AbstractModelConverter<ThingOrBuilder>, 052 ConverterInto<ThingOrBuilder, RDFHandler> { 053 054 private final ValueFactory vf; 055 056 public ProtoThingRdfConverter(ValueFactory vf) { 057 this.vf = vf; 058 } 059 060 public ProtoThingRdfConverter() { 061 // just like in Values.VALUE_FACTORY 062 this(new ValidatingValueFactory(SimpleValueFactory.getInstance())); 063 } 064 065 @Override 066 public boolean convertInto(ThingOrBuilder from, RDFHandler into) throws ConversionException { 067 into.startRDF(); 068 Namespacer.setNamespaces(from, into); 069 Map<BNode, Thing> containedThings = new HashMap<>(); 070 convertInto(null, from, into, containedThings); 071 into.endRDF(); 072 return true; 073 } 074 075 private void convertInto( 076 String bNodeID, ThingOrBuilder from, RDFHandler into, Map<BNode, Thing> containedThings) 077 throws ConversionException { 078 Resource subject; 079 var iri = from.getIri(); 080 if (!Strings.isNullOrEmpty(iri)) { 081 subject = vf.createIRI(iri); 082 } else if (!Strings.isNullOrEmpty(bNodeID)) { 083 subject = vf.createBNode(bNodeID); 084 } else { 085 throw new IllegalStateException(from.toString()); 086 } 087 for (var field : from.getPropertiesMap().entrySet()) { 088 IRI predicate = vf.createIRI(field.getKey()); 089 for (var object : convert(field.getValue(), containedThings)) { 090 Statement statement = vf.createStatement(subject, predicate, object); 091 into.handleStatement(statement); 092 } 093 } 094 095 for (var containedThing : containedThings.entrySet()) { 096 Map<BNode, Thing> deeperContainedThings = new HashMap<>(); 097 convertInto( 098 containedThing.getKey().getID(), 099 containedThing.getValue(), 100 into, 101 deeperContainedThings); 102 } 103 } 104 105 private Iterable<org.eclipse.rdf4j.model.Value> convert( 106 dev.enola.thing.proto.Value value, Map<BNode, Thing> containedThings) { 107 return switch (value.getKindCase()) { 108 case LINK -> singleton(vf.createIRI(value.getLink())); 109 110 case STRING -> singleton(vf.createLiteral(value.getString())); 111 112 case LITERAL -> { 113 var literal = value.getLiteral(); 114 yield singleton( 115 vf.createLiteral(literal.getValue(), vf.createIRI(literal.getDatatype()))); 116 } 117 118 case LANG_STRING -> { 119 LangString langString = value.getLangString(); 120 yield singleton(vf.createLiteral(langString.getText(), langString.getLang())); 121 } 122 123 case STRUCT -> { 124 BNode bNode; 125 var containedThing = value.getStruct(); 126 var containedThingIRI = containedThing.getIri(); 127 if (!Strings.isNullOrEmpty(containedThingIRI)) { 128 bNode = vf.createBNode(containedThingIRI); 129 } else { 130 bNode = vf.createBNode(); 131 } 132 containedThings.put(bNode, containedThing); 133 yield singleton(bNode); 134 } 135 136 case LIST -> { 137 // TODO value.getList().getValuesList().stream().map(eValue -> convert(eValue,? 138 var enolaValues = value.getList().getValuesList(); 139 var rdfValues = 140 ImmutableList.<org.eclipse.rdf4j.model.Value>builderWithExpectedSize( 141 enolaValues.size()); 142 for (var enolaValue : enolaValues) { 143 var rdfValue = convert(enolaValue, containedThings); 144 // Not 100% sure if addAll() is "correct" here... 145 rdfValues.addAll(rdfValue); 146 } 147 yield rdfValues.build(); 148 } 149 150 case KIND_NOT_SET -> throw new IllegalArgumentException(value.toString()); 151 }; 152 } 153}