001/* 002 * SPDX-License-Identifier: Apache-2.0 003 * 004 * Copyright 2023-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.cli; 019 020import com.google.common.collect.ImmutableList; 021 022import dev.enola.common.context.TLC; 023import dev.enola.common.io.iri.URIs; 024import dev.enola.common.io.resource.FileDescriptorResource; 025import dev.enola.common.io.resource.stream.GlobResolvers; 026import dev.enola.core.EnolaServiceProvider; 027import dev.enola.core.grpc.EnolaGrpcClientProvider; 028import dev.enola.core.grpc.EnolaGrpcInProcess; 029import dev.enola.core.grpc.ServiceProvider; 030import dev.enola.core.proto.EnolaServiceGrpc.EnolaServiceBlockingStub; 031import dev.enola.data.ProviderFromIRI; 032import dev.enola.data.Trigger; 033import dev.enola.data.iri.NamespaceConverter; 034import dev.enola.datatype.DatatypeRepository; 035import dev.enola.infer.rdf.RDFSPropertyTrigger; 036import dev.enola.thing.impl.MutableThing; 037import dev.enola.thing.java.ProxyTBF; 038import dev.enola.thing.java.TBF; 039import dev.enola.thing.message.AlwaysThingProviderAdapter; 040import dev.enola.thing.metadata.ThingMetadataProvider; 041import dev.enola.thing.proto.Thing; 042import dev.enola.thing.repo.*; 043import dev.enola.thing.template.TemplateThingRepository; 044import dev.enola.thing.validation.LoggingCollector; 045import dev.enola.thing.validation.Validators; 046 047import org.jspecify.annotations.Nullable; 048 049import picocli.CommandLine; 050import picocli.CommandLine.ArgGroup; 051import picocli.CommandLine.Model.CommandSpec; 052import picocli.CommandLine.Option; 053import picocli.CommandLine.Spec; 054 055import java.net.URI; 056import java.util.List; 057import java.util.stream.Stream; 058 059public abstract class CommandWithModel extends CommandWithResourceProviderAndLoader { 060 061 protected EnolaServiceProvider esp; 062 063 @Spec CommandSpec spec; 064 @ArgGroup @Nullable ModelOrServer group; 065 066 @CommandLine.Option( 067 names = {"--validate"}, 068 negatable = true, 069 required = true, 070 defaultValue = "false", // TODO Enable Model validation by default 071 showDefaultValue = CommandLine.Help.Visibility.ALWAYS, 072 description = "Whether validation errors in loaded models should stop & exit") 073 boolean validate; 074 075 private EnolaServiceBlockingStub gRPCService; 076 077 // TODO Turn remote service encapsulation upside down (as-is this "exception" is strange) 078 protected TemplateThingRepository templateService; 079 080 @Override 081 public final void run() throws Exception { 082 super.run(); 083 try (var ctx1 = TLC.open()) { 084 setup(ctx1); 085 086 if (group == null) { 087 group = new ModelOrServer(); 088 group.load = List.of(); 089 } 090 091 // TODO Move elsewhere for continuous ("shell") mode, as this is "expensive". 092 ServiceProvider grpc = null; 093 if (group.load != null) { 094 ImmutableList<Trigger<? extends dev.enola.thing.Thing>> triggers = 095 ImmutableList.of(new RDFSPropertyTrigger()); 096 ThingMemoryRepositoryROBuilder store = new ThingMemoryRepositoryROBuilder(triggers); 097 for (var trigger : triggers) { 098 ((ThingTrigger<?>) trigger).setRepo(store); 099 } 100 101 try (var ctx2 = TLC.open()) { 102 ctx2.push(ThingProvider.class, new AlwaysThingRepositoryStore(store)); 103 ctx2.push(TBF.class, new ProxyTBF(MutableThing.FACTORY)); 104 var loader = loader(); 105 var fgrp = new GlobResolvers(); 106 for (var globIRI : group.load) { 107 try (var stream = fgrp.get(globIRI)) { 108 loader.convertIntoOrThrow(stream, store); 109 } 110 } 111 } 112 var repo = store.build(); 113 114 if (validate) { 115 var c = new LoggingCollector(); 116 var v = new Validators(repo); 117 v.validate(repo, c); 118 if (c.hasMessages()) { 119 System.err.println( 120 "Loaded models have validation errors; use -v to show them (or use" 121 + " --no-validate to disable)"); 122 System.exit(7); 123 } 124 } 125 126 TemplateThingRepository templateThingRepository = new TemplateThingRepository(repo); 127 templateService = templateThingRepository; 128 ThingsProvider thingsProvider = 129 new ThingsProvider() { 130 @Override 131 public Stream<dev.enola.thing.Thing> getThings(String iri) { 132 throw new UnsupportedOperationException("TODO"); 133 } 134 }; 135 esp = new EnolaServiceProvider(thingsProvider, templateThingRepository, rp); 136 var enolaService = esp.getEnolaService(); 137 grpc = new EnolaGrpcInProcess(esp, enolaService, false); // direct, single-threaded! 138 gRPCService = grpc.get(); 139 140 } else if (group.server != null) { 141 grpc = new EnolaGrpcClientProvider(group.server, false); // direct, single-threaded! 142 gRPCService = grpc.get(); 143 } 144 145 try { 146 run(gRPCService); 147 } finally { 148 if (grpc != null) grpc.close(); 149 } 150 } 151 } 152 153 // TODO Move this to class EnolaProvider? 154 protected ThingMetadataProvider getMetadataProvider(ProviderFromIRI<Thing> thingProvider) { 155 return new ThingMetadataProvider( 156 new AlwaysThingProviderAdapter(thingProvider, DatatypeRepository.CTX), 157 NamespaceConverter.CTX); 158 } 159 160 protected abstract void run(EnolaServiceBlockingStub service) throws Exception; 161 162 static class LoadableModelURIs { 163 164 @Option( 165 names = {"--load", "-l"}, 166 description = "URI Glob of Models to load (e.g. file:\"**.ttl\")") 167 /* TODO @Nullable */ java.util.List<String> load; 168 } 169 170 static class ModelOrServer extends LoadableModelURIs { 171 172 @Option( 173 names = {"--server", "-s"}, 174 required = true, 175 description = "Target of an Enola gRPC Server (e.g. localhost:7070)") 176 @Nullable String server; 177 } 178 179 static class Output { 180 // Default command output destination is STDOUT. 181 // NB: "fd:1" normally (in ResourceProviders) is FileDescriptorResource, 182 // but CommandWithIRI "hacks" this and uses WriterResource, for "testability". 183 static final URI DEFAULT_OUTPUT_URI = FileDescriptorResource.STDOUT_URI; 184 185 @Option( 186 names = {"--output", "-o"}, 187 required = true, 188 defaultValue = FileDescriptorResource.STDOUT, 189 showDefaultValue = CommandLine.Help.Visibility.ALWAYS, 190 description = 191 "URI (base) of where to write output/s; e.g. file:/tmp or " 192 + FileDescriptorResource.STDOUT) 193 URI output; 194 195 static URI get(Output output) { 196 if (output == null) return FileDescriptorResource.STDOUT_URI; 197 if (output != null && output.output == null) return FileDescriptorResource.STDOUT_URI; 198 return URIs.absolutify(output.output); 199 } 200 } 201}