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.template; 019 020import com.github.fge.uritemplate.URITemplate; 021import com.github.fge.uritemplate.URITemplateException; 022import com.github.fge.uritemplate.URITemplateParseException; 023import com.google.common.base.Function; 024import com.google.common.collect.Iterables; 025 026import dev.enola.common.collect.MoreIterables; 027import dev.enola.data.iri.template.URITemplateMatcherChain; 028import dev.enola.data.iri.template.VariableMaps; 029import dev.enola.thing.*; 030import dev.enola.thing.impl.ImmutableThing; 031import dev.enola.thing.repo.ThingRepository; 032 033import org.jspecify.annotations.Nullable; 034 035import java.util.*; 036import java.util.AbstractMap.SimpleImmutableEntry; 037 038public class TemplateThingRepository implements ThingRepository, TemplateService { 039 040 private record Match(String iriTemplate, Function<Map<String, String>, Thing> function) {} 041 042 private final ThingRepository delegate; 043 private final URITemplateMatcherChain<Match> iriTemplateChain; 044 045 public TemplateThingRepository(ThingRepository delegate) { 046 this.delegate = delegate; 047 int size = MoreIterables.sizeIfKnown(delegate.list()).orElse(42); 048 var iriTemplateChainBuilder = URITemplateMatcherChain.<Match>builderWithExpectedSize(size); 049 for (var thing : delegate.list()) { 050 if (!thing.isIterable(KIRI.RDF.TYPE) 051 && KIRI.RDFS.CLASS.equals(thing.getString(KIRI.RDF.TYPE))) { 052 thing.getOptional(KIRI.E.IRI_TEMPLATE_PROPERTY, String.class) 053 .ifPresent( 054 iriTemplate -> 055 iriTemplateChainBuilder.add( 056 iriTemplate, gen(iriTemplate, thing))); 057 } 058 } 059 this.iriTemplateChain = iriTemplateChainBuilder.build(); 060 } 061 062 private Match gen(String classIRITemplate, Thing rdfClass) { 063 Set<SimpleImmutableEntry<String, URITemplate>> set = new HashSet<>(); 064 for (String predicateIRI : rdfClass.predicateIRIs()) { 065 if (KIRI.E.IRI_TEMPLATE_DATATYPE.equals(rdfClass.datatype(predicateIRI))) { 066 var uriTemplate = newURITemplate(rdfClass.getString(predicateIRI)); 067 set.add(new SimpleImmutableEntry<String, URITemplate>(predicateIRI, uriTemplate)); 068 } 069 } 070 var templatePredicates = Collections.unmodifiableSet(set); 071 072 var classURITemplate = newURITemplate(classIRITemplate); 073 return new Match( 074 classIRITemplate, 075 params -> { 076 try { 077 var varMap = VariableMaps.from(params); 078 var builder = ImmutableThing.builder(); 079 var newIRI = Templates.unescapeURL(classURITemplate.toString(varMap)); 080 builder.iri(newIRI); 081 builder.set(KIRI.RDF.TYPE, new Link(rdfClass.iri())); 082 for (var templatePredicate : templatePredicates) { 083 var predicateIRI = templatePredicate.getKey(); 084 var predicateURITemplate = templatePredicate.getValue(); 085 var link = Templates.unescapeURL(predicateURITemplate.toString(varMap)); 086 builder.set(predicateIRI, new Link(link)); 087 } 088 089 return builder.build(); 090 } catch (URITemplateException e) { 091 throw new IllegalArgumentException(rdfClass.iri(), e); 092 } 093 }); 094 } 095 096 private URITemplate newURITemplate(String template) { 097 try { 098 return new URITemplate(template); 099 } catch (URITemplateParseException e) { 100 throw new IllegalArgumentException(template + " invalid IRI Template", e); 101 } 102 } 103 104 @Override 105 public Iterable<String> listIRI() { 106 return Iterables.concat(iriTemplateChain.listTemplates(), delegate.listIRI()); 107 } 108 109 @Override 110 public @Nullable Thing get(String iri) { 111 // TODO Check delegate first, and merge() if found... with TDD! 112 var optEntry = iriTemplateChain.match(iri); 113 if (optEntry.isPresent()) { 114 var entry = optEntry.get(); 115 var match = entry.getKey(); 116 var params = entry.getValue(); 117 var function = match.function; 118 return function.apply(params); 119 } else return delegate.get(iri); 120 } 121 122 @Override 123 public Optional<Breakdown> breakdown(String nonTemplateIRI) { 124 if (Templates.hasVariables(nonTemplateIRI)) 125 throw new IllegalArgumentException("Template: " + nonTemplateIRI); 126 var optEntry = iriTemplateChain.match(nonTemplateIRI); 127 if (optEntry.isPresent()) { 128 var entry = optEntry.get(); 129 var match = entry.getKey(); 130 var params = entry.getValue(); 131 var iriTemplate = match.iriTemplate; 132 return Optional.of(new Breakdown(iriTemplate, params)); 133 } else return Optional.empty(); 134 } 135}