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}