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.thing.message;
019
020import static java.util.Objects.requireNonNull;
021
022import com.google.common.base.MoreObjects;
023import com.google.common.collect.ImmutableCollection;
024import com.google.common.collect.ImmutableList;
025import com.google.common.collect.ImmutableMap;
026import com.google.common.collect.ImmutableSet;
027
028import dev.enola.common.convert.ConversionException;
029import dev.enola.datatype.DatatypeRepository;
030import dev.enola.thing.LangString;
031import dev.enola.thing.Link;
032import dev.enola.thing.PredicatesObjects;
033import dev.enola.thing.impl.IImmutablePredicatesObjects;
034import dev.enola.thing.impl.ImmutablePredicatesObjects;
035import dev.enola.thing.proto.Value;
036
037import org.jspecify.annotations.Nullable;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041import java.util.Objects;
042
043public class PredicatesObjectsAdapter implements IImmutablePredicatesObjects {
044
045    // TODO This is too similar to ProtoThingIntoJavaThingBuilderConverter, and must be merged
046
047    private static final Logger LOG = LoggerFactory.getLogger(ThingAdapter.class);
048
049    protected final dev.enola.thing.proto.Thing proto;
050
051    @SuppressWarnings("Immutable")
052    protected final DatatypeRepository datatypeRepository;
053
054    public PredicatesObjectsAdapter(
055            dev.enola.thing.proto.Thing proto, DatatypeRepository datatypeRepository) {
056        this.proto = requireNonNull(proto, "proto");
057        // TODO requireNonNull(datatypeRepository, "datatypeRepository");
058        this.datatypeRepository = datatypeRepository;
059    }
060
061    @Override
062    public ImmutableSet<String> predicateIRIs() {
063        return ImmutableSet.copyOf(proto.getPropertiesMap().keySet());
064    }
065
066    @Override
067    @SuppressWarnings("unchecked")
068    public <T> @Nullable T get(String predicateIRI) {
069        var value = proto.getPropertiesMap().get(predicateIRI);
070        if (value != null) return (T) value(value);
071        else return null;
072    }
073
074    @Override
075    public ImmutableMap<String, Object> properties() {
076        var predicateIRIs = predicateIRIs();
077        var builder = ImmutableMap.<String, Object>builderWithExpectedSize(predicateIRIs.size());
078        for (var predicateIRI : predicateIRIs) {
079            var value = get(predicateIRI);
080            if (value != null) builder.put(predicateIRI, value);
081        }
082        return builder.build();
083    }
084
085    @Override
086    public ImmutableMap<String, String> datatypes() {
087        var predicateIRIs = predicateIRIs();
088        var builder = ImmutableMap.<String, String>builderWithExpectedSize(predicateIRIs.size());
089        for (var predicateIRI : predicateIRIs) {
090            var datatype = datatype(predicateIRI);
091            if (datatype != null) builder.put(predicateIRI, datatype);
092        }
093        return builder.build();
094    }
095
096    @Override
097    public @Nullable String datatype(String predicateIRI) {
098        var value = proto.getPropertiesMap().get(predicateIRI);
099        if (Value.KindCase.LITERAL.equals(value.getKindCase()))
100            return value.getLiteral().getDatatype();
101        else return null;
102    }
103
104    private @Nullable Object value(dev.enola.thing.proto.Value value) {
105        return switch (value.getKindCase()) {
106            case STRING -> value.getString();
107            case LITERAL -> literal(value.getLiteral());
108            case LANG_STRING -> langString(value.getLangString());
109            case LINK -> new Link(value.getLink());
110            case LIST -> listOrSet(value.getList());
111            case STRUCT -> map(value.getStruct());
112            case KIND_NOT_SET -> null;
113        };
114    }
115
116    private dev.enola.thing.LangString langString(dev.enola.thing.proto.Value.LangString proto) {
117        return new LangString(proto.getText(), proto.getLang());
118    }
119
120    private @Nullable Object literal(dev.enola.thing.proto.Value.Literal literal) {
121        var literalValue = literal.getValue();
122        var datatypeIRI = literal.getDatatype();
123        var datatype = datatypeRepository.get(datatypeIRI);
124        if (datatype == null) return new dev.enola.thing.Literal(literalValue, datatypeIRI);
125        try {
126            return datatype.stringConverter().convertFrom(literalValue);
127        } catch (ConversionException e) {
128            LOG.warn("Failed to convert '{}'' of datatype {}", literalValue, datatypeIRI, e);
129            return new dev.enola.thing.Literal(literalValue, datatypeIRI);
130        }
131    }
132
133    private ImmutableCollection<?> listOrSet(dev.enola.thing.proto.Value.List list) {
134        // TODO Make this lazier... only convert object when they're actually used
135        var protoValues = list.getValuesList();
136        ImmutableCollection.Builder<Object> collectionBuilder;
137        if (list.getOrdered()) {
138            collectionBuilder = ImmutableList.builderWithExpectedSize(protoValues.size());
139        } else {
140            collectionBuilder = ImmutableSet.builderWithExpectedSize(protoValues.size());
141        }
142
143        for (var protoValue : protoValues) {
144            var value = value(protoValue);
145            if (value != null) collectionBuilder.add(value);
146        }
147        return collectionBuilder.build();
148    }
149
150    private PredicatesObjectsAdapter map(dev.enola.thing.proto.Thing struct) {
151        return new PredicatesObjectsAdapter(struct, datatypeRepository);
152    }
153
154    @Override
155    public PredicatesObjects.Builder<? extends PredicatesObjects> copy() {
156        // TODO Alternatively to this approach, we could also wrap a Proto Thing Builder
157        var properties = properties();
158        var builder = ImmutablePredicatesObjects.builderWithExpectedSize(properties.size());
159        properties.forEach(
160                (predicate, value) -> set(builder, predicate, value, datatype(predicate)));
161        return builder;
162    }
163
164    @SuppressWarnings("Immutable") // TODO Object value https://errorprone.info/bugpattern/Immutable
165    private void set(
166            Builder<? extends IImmutablePredicatesObjects> builder,
167            String predicate,
168            Object value,
169            @Nullable String datatype) {
170        builder.set(predicate, value, datatype);
171    }
172
173    @Override
174    public boolean equals(Object obj) {
175        if (obj == this) return true;
176        // NO NEED: if (obj == null) return false;
177        // NOT:     if (getClass() != obj.getClass()) return false;
178        if (!(obj instanceof PredicatesObjectsAdapter other)) return false;
179        if (obj instanceof ThingAdapter) return false; // skipcq: JAVA-W0095
180        // The skipcq works around an apparent SpotBugs error; because ThingAdapter *IS* a subtype?!
181        // https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html#eq-equals-checks-for-incompatible-operand-eq-check-for-operand-not-compatible-with-this
182        return this.proto.equals(other.proto)
183                && this.datatypeRepository.equals(other.datatypeRepository);
184    }
185
186    @Override
187    public int hashCode() {
188        return Objects.hash(proto, datatypeRepository);
189    }
190
191    @Override
192    public String toString() {
193        return MoreObjects.toStringHelper(this).add("properties", properties()).toString();
194    }
195}