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.model.enola.meta.io; 019 020import dev.enola.common.context.TLC; 021import dev.enola.common.io.resource.ReadableResource; 022import dev.enola.common.yamljson.YAML; 023import dev.enola.data.iri.namespace.repo.EmptyNamespaceRepository; 024import dev.enola.data.iri.namespace.repo.NamespaceRepository; 025import dev.enola.data.iri.namespace.repo.NamespaceRepositoryBuilder; 026import dev.enola.model.enola.HasName; 027import dev.enola.model.enola.meta.*; 028import dev.enola.model.enola.meta.Class; 029import dev.enola.thing.repo.ThingRepositoryStore; 030 031import org.jspecify.annotations.Nullable; 032 033import java.io.IOException; 034import java.net.URI; 035import java.util.List; 036import java.util.Map; 037import java.util.concurrent.atomic.AtomicReference; 038import java.util.function.BiConsumer; 039import java.util.function.Consumer; 040 041public class SchemaIO { 042 043 // TODO Use dev.enola.thing.validation and errors.esch.yaml instead of IllegalArgumentException 044 045 public Schema readYAML(ReadableResource resource) throws IOException { 046 // TODO Avoid AtomicReference 047 AtomicReference<Schema> schema = new AtomicReference<>(); 048 readYAML(resource, schema1 -> schema.set(schema1)); 049 return schema.get(); 050 } 051 052 public void readYAML(ReadableResource resource, Consumer<Schema> schemay) throws IOException { 053 var repo = new MetaThingByIdProvider.Builder(); 054 YAML.readSingleMap( 055 resource, map -> readSchema(map, repo, schema -> schemay.accept(schema))); 056 } 057 058 private void readSchema( 059 Map<?, ?> map, MetaThingByIdProvider.Builder repo, Consumer<Schema> schemay) { 060 var id = getRemoveString(map, "id"); 061 if (id == null) throw new IllegalArgumentException("Mandatory schema id: is missing"); 062 var schema = repo.schema(id); 063 schema.name(getRemoveString(map, "name")); 064 065 var iri = getRemoveString(map, "iri"); 066 if (iri == null) throw new IllegalArgumentException("Mandatory schema iri: is missing"); 067 if (iri.endsWith("/")) 068 throw new IllegalArgumentException("Schema's iri: must NOT end with slash: " + iri); 069 schema.iri(iri); 070 // TODO Re-review this pretty ugly "hack" later... 071 TLC.get(ThingRepositoryStore.class).store(schema); 072 073 schema.description(getRemoveString(map, "description")); 074 schema.java_package(getRemoveString(map, "java:package")); 075 076 var nsr = namespaceRepository(map); 077 // TODO Push onto TLC, and use when encountering namespaced IDs? 078 079 // TODO imports 080 081 var datatypes = getRemoveMap(map, "datatypes"); 082 if (datatypes != null) readDatatypes(datatypes, schema, repo); 083 084 var properties = getRemoveMap(map, "properties"); 085 if (properties != null) readProperties(properties, schema, repo); 086 087 var classes = getRemoveMap(map, "classes"); 088 if (classes != null) readClasses(classes, schema, repo); 089 090 checkEmpty(map); 091 schemay.accept(schema.build()); 092 } 093 094 private NamespaceRepository namespaceRepository(Map<?, ?> map) { 095 var prefixes = getRemoveMap(map, ".prefixes"); 096 if (prefixes == null) return EmptyNamespaceRepository.INSTANCE; 097 var nsrb = new NamespaceRepositoryBuilder(); 098 prefixes.forEach((prefix, iri) -> nsrb.store(prefix.toString(), iri.toString())); 099 return nsrb.build(); 100 } 101 102 private void readClasses( 103 Map<?, ?> map, Schema.Builder<?> schema, MetaThingByIdProvider.Builder repo) { 104 readMaps( 105 map, 106 schema, 107 (name, classMap) -> { 108 var clazz = repo.clazz(schema, name); 109 readCommon(classMap, clazz, schema.iri()); 110 111 clazz.iriTemplate(getRemoveString(classMap, "iri_template")); 112 113 var parentsNames = getRemoveList(classMap, "parents"); 114 if (parentsNames != null) 115 for (var parentName : parentsNames) 116 clazz.addParent(repo.clazz(schema, parentName.toString())); 117 118 var idPropertiesMap = asMap(getRemoveMap(classMap, "ids")); 119 if (idPropertiesMap != null) 120 readClassProperties(idPropertiesMap, clazz, schema, repo, true); 121 122 var propertiesMap = asMap(getRemoveMap(classMap, "properties")); 123 if (propertiesMap != null) 124 readClassProperties(propertiesMap, clazz, schema, repo, false); 125 126 checkEmpty(classMap); 127 schema.addSchemaClass(clazz); 128 }); 129 } 130 131 private void readDatatypes( 132 Map<?, ?> map, Schema.Builder<?> schema, MetaThingByIdProvider.Builder repo) { 133 readMaps( 134 map, 135 schema, 136 (name, datatypeMap) -> { 137 var datatype = repo.datatype(schema, name); 138 readCommon(datatypeMap, datatype, schema.iri()); 139 datatype.java(getRemoveString(datatypeMap, "java:type")); 140 datatype.proto(getRemoveString(datatypeMap, "proto")); 141 142 var xsd = getRemoveString(datatypeMap, "xsd"); 143 // TODO Resolve CURIE, if it is one - but how do we know? 144 if (xsd != null) datatype.xsd(URI.create(xsd)); 145 146 var parentName = getRemoveString(datatypeMap, "parent"); 147 if (parentName != null) datatype.parent(repo.datatype(schema, parentName)); 148 149 checkEmpty(datatypeMap); 150 schema.addSchemaDatatype(datatype); 151 }); 152 } 153 154 private void readProperties( 155 Map<?, ?> map, Schema.Builder<?> schema, MetaThingByIdProvider.Builder repo) { 156 readMaps( 157 map, 158 schema, 159 (name, propertyMap) -> { 160 var property = repo.property(schema, name); 161 readProperty(propertyMap, property, schema, repo); 162 schema.addSchemaProperty(property); 163 }); 164 } 165 166 private void readClassProperties( 167 Map<?, ?> map, 168 Class.Builder<?> clazz, 169 Schema schema, 170 MetaThingByIdProvider.Builder repo, 171 boolean isID) { 172 map.forEach( 173 (name, value) -> { 174 if (value == null) 175 // TODO throw new IllegalStateException("TODO Implement Property lookup: " + 176 // name); 177 ; 178 else { 179 var property = repo.property(schema, name.toString()); 180 if (value instanceof Map<?, ?> propertyMap) 181 readProperty(propertyMap, property, schema, repo); 182 else if (value instanceof String propertyDatatypeName) 183 setDatatype(property, propertyDatatypeName, repo); 184 else throw new IllegalArgumentException(value.toString()); 185 // TODO property.multiplicity(...) 186 if (isID) clazz.addClassIdProperty(property); 187 else clazz.addClassProperty(property); 188 } 189 }); 190 } 191 192 private void setDatatype( 193 Property.Builder<?> property, String datatypeName, MetaThingByIdProvider.Builder repo) { 194 property.datatype(repo.datatype(property.schema(), datatypeName)); 195 } 196 197 private void readProperty( 198 Map<?, ?> map, 199 Property.Builder<?> property, 200 Schema schema, 201 MetaThingByIdProvider.Builder repo) { 202 readCommon(map, property, schema.iri()); 203 setDatatype(property, getRemoveString(map, "type"), repo); 204 var parentName = getRemoveString(map, "parent"); 205 if (parentName != null) property.parent(repo.property(schema, parentName)); 206 // TODO getRemoveString(map, "inverse") 207 checkEmpty(map); 208 } 209 210 private void readCommon(Map<?, ?> map, Common.Builder<?> common, String schemaIRI) { 211 common.iri(getRemoveString(map, "iri")); 212 setIRI(common, schemaIRI); 213 214 common.label(getRemoveString(map, "label")); 215 common.description(getRemoveString(map, "description")); 216 // TODO Handle description-md VS description... 217 common.description(getRemoveString(map, "description-md")); 218 // TODO Resolve enola:emoji vs label / description (without enola:) inconsistency... 219 common.emoji(getRemoveString(map, "enola:emoji")); 220 } 221 222 private void readMaps( 223 Map<?, ?> map, Schema.Builder<?> schema, BiConsumer<String, Map<?, ?>> mapper) { 224 map.forEach( 225 (name, object) -> { 226 var innerMap = asMap(object); 227 if (innerMap != null) mapper.accept(name.toString(), innerMap); 228 }); 229 } 230 231 // TODO Remove, since MetaThingByIdProvider already sets iri() ? 232 private void setIRI(HasName.Builder named, String schemaIRI) { 233 // NOT if (named.iri() == null) { 234 named.iri(schemaIRI + "/" + named.name()); 235 } 236 237 private /* TODO @Nullable */ String getRemoveString(Map<?, ?> map, String name) { 238 var value = map.get(name); 239 map.remove(name); // skipcq: JAVA-E1036 240 return value != null ? value.toString() : null; 241 } 242 243 private @Nullable Map<?, ?> getRemoveMap(Map<?, ?> map, String name) { 244 var value = map.get(name); 245 map.remove(name); // skipcq: JAVA-E1036 246 return asMap(value); 247 } 248 249 private @Nullable List<?> getRemoveList(Map<?, ?> map, String name) { 250 var value = map.get(name); 251 map.remove(name); // skipcq: JAVA-E1036 252 return asList(value); 253 } 254 255 private @Nullable Map<?, ?> asMap(@Nullable Object object) { 256 if (object == null) return null; 257 if (object instanceof Map<?, ?> map) return map; 258 throw new IllegalArgumentException( 259 "Should be Map, but is: " + object.getClass() + " " + object); 260 } 261 262 private @Nullable List<?> asList(@Nullable Object object) { 263 if (object == null) return null; 264 if (object instanceof List<?> list) return list; 265 return List.of(object); 266 } 267 268 // TODO Remove when finally fully switching to Thing, where it's *OK* to have extra! 269 private void checkEmpty(Map<?, ?> map) { 270 // Remove a few "special" entries: 271 map.remove("@context"); // skipcq: JAVA-E1036 272 map.remove("$schema"); // skipcq: JAVA-E1036 273 274 if (!map.isEmpty()) { 275 throw new IllegalArgumentException("Unknown properties: " + map); 276 } 277 } 278}