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}