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.metadata;
019
020import com.google.common.collect.ImmutableList;
021
022import dev.enola.thing.KIRI;
023import dev.enola.thing.Link;
024import dev.enola.thing.Thing;
025
026import java.net.URI;
027import java.util.ArrayList;
028import java.util.Optional;
029
030/**
031 * Provider of _"hierarchical"_ (e.g. parent / child) relationships between Things.
032 *
033 * <p>This can be used e.g. to "cluster" things in visual graph diagram representations.
034 */
035public class ThingHierarchyProvider {
036
037    private final String description;
038    private final Iterable<String> propertyIRIs;
039
040    public ThingHierarchyProvider() {
041        // TODO Same as in ThingTimeProvider, properties eventually won't be hard-coded anymore
042        this(
043                "By Parent Hierarchy:",
044                ImmutableList.of(
045                        "https://enola.dev/parent",
046                        "https://enola.dev/files/Node/parent", // TODO Node.parent_IRI,
047                        KIRI.RDF.TYPE,
048                        "http://www.w3.org/2000/01/rdf-schema#subClassOf",
049                        "http://www.w3.org/2000/01/rdf-schema#subPropertyOf",
050                        "https://enola.dev/origin"));
051    }
052
053    public ThingHierarchyProvider(String description, Iterable<String> propertyIRIs) {
054        this.description = description;
055        this.propertyIRIs = propertyIRIs;
056    }
057
058    public String description() {
059        return description;
060    }
061
062    public Iterable<String> parents(Thing thing) {
063        var list = new ArrayList<String>();
064        for (var propertyIRI : propertyIRIs) {
065            if (thing.isIterable(propertyIRI)) {
066                var parents = thing.get(propertyIRI, Iterable.class);
067                if (parents == null) continue;
068                for (var parent : parents) {
069                    // TODO Same story as in GraphvizGenerator
070                    list.add(parent.toString());
071                }
072            } else {
073                var parent = thing.getString(propertyIRI);
074                if (parent != null) list.add(parent);
075            }
076        }
077        return list;
078    }
079
080    public Optional<String> parent(Thing thing) {
081        for (var propertyIRI : propertyIRIs) {
082            if (thing.isIterable(propertyIRI)) {
083                var parents = thing.get(propertyIRI, Iterable.class);
084                if (parents == null) continue;
085                var iterator = parents.iterator();
086                if (!iterator.hasNext()) continue;
087                var parent = iterator.next();
088                return switch (parent) {
089                    // TODO This is a kind of converter, and shouldn't be here?
090                    case String string -> Optional.of(string);
091                    case URI uri -> Optional.of(uri.toString());
092                    case Link link -> Optional.of(link.iri());
093                    default ->
094                            throw new IllegalStateException(
095                                    "Parent of unexpected type: "
096                                            + parent.getClass()
097                                            + " : "
098                                            + parent);
099                };
100            } else {
101                var parent = thing.getString(propertyIRI);
102                if (parent != null) return Optional.of(parent);
103            }
104        }
105        return Optional.empty();
106    }
107}