diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/ErrorMapper.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/ErrorMapper.java index 487a25c0..ea4acc83 100644 --- a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/ErrorMapper.java +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/ErrorMapper.java @@ -14,13 +14,19 @@ import org.glassfish.hk2.api.MultiException; import org.glassfish.jersey.server.spi.ResponseErrorMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import uk.co.amrc.factoryplus.metadb.db.Err; public class ErrorMapper { + public static final Logger log = LoggerFactory.getLogger(ErrorMapper.class); + public static Response clientError (Err.ClientError err) { /* XXX we should allow the error to include more fields here */ + log.info("Returning error: {}", err); var json = Json.createValue(err.getMessage()); return Response.status(err.statusCode()) .entity(json) diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/Sparql.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/Sparql.java index ec3b1b7f..7c5189d9 100644 --- a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/Sparql.java +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/Sparql.java @@ -126,7 +126,7 @@ private static record QueryHandler ( @Consumes("application/sparql-update") public void update (String update) { - store.executeWrite(() -> + store.requestExecute(req -> UpdateAction.parseExecute(update, store.dataset())); } @@ -143,7 +143,7 @@ public StreamingOutput sparql (String queryString) throws WebApplicationExceptio var lang = handler.content().acceptLang(req); - return store.calculateRead(() -> { + return store.requestRead(req -> { try (var qexec = QueryExecutionFactory.create(query, store.dataset())) { return handler.handle().apply(qexec, lang); } @@ -226,7 +226,7 @@ public void graphPost ( var lang = RDF_HANDLER.contentLang(type); var graph = resolveGraph(true); - store.executeWrite(() -> { + store.requestExecute(req -> { readToGraph(graph, rdf, lang); /* This refreshes the inferences because we have been poking * around behind its back. Strictly this is only needed when @@ -244,7 +244,7 @@ public void graphPut ( var lang = RDF_HANDLER.contentLang(type); var graph = resolveGraph(true); - store.executeWrite(() -> { + store.requestExecute(req -> { graph.removeAll(); readToGraph(graph, rdf, lang); store.derived().rebind(); diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/V2Config.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/V2Config.java index 7fb8f07f..90317249 100644 --- a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/V2Config.java +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/V2Config.java @@ -35,23 +35,21 @@ public class V2Config { @PathParam("app") UUID app; @PathParam("object") UUID obj; - private ConfigEntry configEntry () - { - return store.configEntry(app, obj); - } - @GET public Response get () { log.info("Get config for {}/{}", app, obj); - var entry = store.calculateRead(() -> { - return configEntry().getValue(); + var entry = store.requestRead(req -> { + return req.configEntry(app, obj).getValue(); }); return entry - .map(e -> Response.ok(e.value()) - .tag(e.etag()) - .lastModified(Date.from(e.mtime())) - .build()) + .map(e -> { + var res = Response.ok(e.value()) + .tag(e.etag()); + e.mtime().ifPresent(t -> + res.lastModified(Date.from(t))); + return res.build(); + }) .orElseThrow(() -> new WebApplicationException(404)); } @@ -63,8 +61,8 @@ public Response get () public void put (JsonValue config) { log.info("Put config for {}/{}", app, obj); - store.executeWrite(() -> { - configEntry().putValue(config); + store.requestExecute(req -> { + req.configEntry(app, obj).putValue(config); }); } @@ -72,8 +70,8 @@ public void put (JsonValue config) public void delete () { log.info("Delete config for {}/{}", app, obj); - store.executeWrite(() -> { - configEntry().removeValue(); + store.requestExecute(req -> { + req.configEntry(app, obj).removeValue(); }); } @@ -81,8 +79,8 @@ public void delete () public void mergePatch (JsonValue json) { var patch = Json.createMergePatch(json); - store.executeWrite(() -> { - var entry = configEntry(); + store.requestExecute(req -> { + var entry = req.configEntry(app, obj); var o_conf = entry.getValue() .map(e -> e.value()) diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/V2Objects.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/V2Objects.java index 49a7b237..0322b09a 100644 --- a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/V2Objects.java +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/api/V2Objects.java @@ -30,7 +30,8 @@ public class V2Objects { @GET @Path("object") public JsonArray listObjects () { - var objs = db.objectStructure().listObjects(); + var objs = db.requestRead(req -> + req.objectStructure().listObjects()); return Json.createArrayBuilder(objs).build(); } @@ -56,26 +57,28 @@ public JsonValue createObject (JsonObject spec) /* We don't accept any other parameters. The ServiceClient * doesn't pass any anyway. */ - return db.objectStructure().createObject(klass, uuid); + return db.requestWrite(req -> + req.objectStructure().createObject(klass, uuid)); } @DELETE @Path("object/{object}") public void deleteObject (@PathParam("object") UUID uuid) { - db.objectStructure().deleteObject(uuid); + db.requestExecute(req -> req.objectStructure().deleteObject(uuid)); } @GET @Path("object/rank") public JsonArray listRanks () { - var ranks = db.objectStructure().listRanks(); + var ranks = db.requestRead(req -> req.objectStructure().listRanks()); return Json.createArrayBuilder(ranks).build(); } private JsonArray listRelation (String graph, UUID uuid, String relation) { - var members = db.objectStructure() - .listRelation(graph, uuid, relation); + var members = db.requestRead(req -> + req.objectStructure() + .listRelation(graph, uuid, relation)); return Json.createArrayBuilder(members).build(); } @@ -101,8 +104,9 @@ public JsonArray listDirectRelation ( private Response testRelation (String graph, UUID klass, String relation, UUID object) { - var rv = db.objectStructure() - .testRelation(graph, klass, relation, object); + var rv = db.requestRead(req -> + req.objectStructure() + .testRelation(graph, klass, relation, object)); return Response.status(rv ? 204 : 404).build(); } @@ -131,7 +135,8 @@ public void putRelation ( @PathParam("relation") String relation, @PathParam("object") UUID object) { - db.objectStructure().putRelation(klass, relation, object); + db.requestExecute(req -> + req.objectStructure().putRelation(klass, relation, object)); } @DELETE @Path("class/{class}/direct/{relation}/{object}") @@ -140,8 +145,8 @@ public void delRelation ( @PathParam("relation") String relation, @PathParam("object") UUID object) { - db.objectStructure().delRelation(klass, relation, object); + db.requestExecute(req -> + req.objectStructure().delRelation(klass, relation, object)); } - } diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/AppMapper.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/AppMapper.java new file mode 100644 index 00000000..d8b05963 --- /dev/null +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/AppMapper.java @@ -0,0 +1,259 @@ +/* + * Factory+ metadata database + * Structured application mapper + * Copyright 2026 University of Sheffield AMRC + */ + +package uk.co.amrc.factoryplus.metadb.db; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import jakarta.json.*; + +import org.apache.jena.query.*; +import org.apache.jena.rdf.model.*; +import org.apache.jena.update.*; +import org.apache.jena.vocabulary.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.vavr.control.Try; + +public class AppMapper { + private static final Logger log = LoggerFactory.getLogger(AppMapper.class); + + /* Specific updaters for individual Apps. These should be replaced + * with queries generated from schema entries. */ + + private static final Query Q_objectRegistration = Vocab.query(""" + select ?uuid ?rank ?class ?owner ?strict ?deleted + where { + ?obj ?uuid. + optional { + ?obj rdf:type/ ?rank; + / ?class. + } + + optional { ?obj / ?1. } + bind(coalesce(?1, "091e796a-65c0-4080-adff-c3ce01a65b2e") as ?owner) + + optional { ?obj ?2. } + bind(coalesce(?2, false) as ?deleted) + + bind(true as ?strict) + } + """); + + private static final Query Q_generalInfo = Vocab.query(""" + select ?name + where { + ?obj ?name. + } + """); + private static final UpdateRequest D_generalInfo = Vocab.update(""" + delete where { ?obj ?1. }; + """); + private static final UpdateRequest U_generalInfo = Vocab.update(""" + insert { ?obj ?name. } where {}; + """); + + private static final Query Q_sparkplugAddress = Vocab.query(""" + select ?group_id ?node_id ?device_id + where { + ?obj ?group_id. + optional { ?obj ?node_id. } + optional { ?obj ?device_id. } + } + """); + private static final UpdateRequest D_sparkplugAddress = Vocab.update(""" + delete where { ?obj ?1. }; + delete where { ?obj ?2. }; + delete where { ?obj ?3. }; + """); + private static final UpdateRequest U_sparkplugAddress = Vocab.update(""" + insert { + ?obj ?group_id; + ?node_id; + ?device_id. + } where {}; + """); + + /* We have to bypass Jena's RDFDatatype system here and provide our + * own mappings. We cannot map arbitrary objects to JSON, and the + * JSON creation functions will not accept Object. */ + private static final Map>> toJson = Map.of( + XSD.xstring, s -> Optional.of(Json.createValue(s)), + XSD.xint, s -> Try.of(() -> Integer.parseInt(s)) + .toJavaOptional() + .map(Json::createValue), + XSD.xboolean, s -> s.equals("true") ? Optional.of(JsonValue.TRUE) + : s.equals("false") ? Optional.of(JsonValue.FALSE) + : Optional.empty(), + RDF.JSON, Util::readJson); + + private RdfStore db; + + private Map generators = Map.of( + Vocab.App.Registration, Q_objectRegistration, + Vocab.App.Info, Q_generalInfo, + Vocab.App.SparkplugAddr, Q_sparkplugAddress); + private Map deleters = Map.of( + Vocab.App.Info, D_generalInfo, + Vocab.App.SparkplugAddr, D_sparkplugAddress); + private Map updaters = Map.of( + Vocab.App.Info, U_generalInfo, + Vocab.App.SparkplugAddr, U_sparkplugAddress); + + public AppMapper (RdfStore db) + { + this.db = db; + } + + public Optional generateConfig (Resource app, Resource obj) + { + return Optional.ofNullable(generators.get(app)) + .flatMap(q -> db.optionalQuery(q, "obj", obj)) + .map(AppMapper::solutionToJson); + } + + public void deleteConfig (Resource app, Resource obj) + { + if (app.equals(Vocab.App.Registration)) + throw new Err.Immutable(); + + Optional.ofNullable(deleters.get(app)) + .ifPresent(d -> db.runUpdate(d, "obj", obj)); + } + + public void updateConfig (Resource app, Resource obj, JsonValue config) + { + if (app.equals(Vocab.App.Registration)) { + updateRegistration(obj, config); + return; + } + + var sol = jsonToSolution(config); + + deleteConfig(app, obj); + Optional.ofNullable(updaters.get(app)) + .ifPresent(u -> UpdateExecution.dataset(db.dataset()) + .update(u) + .substitution(sol) + .substitution("obj", obj) + .execute()); + } + + /* XXX This assumes JSON schema validation has been performed. + * Currently this is not implemented nor is a schema installed for + * the Registration app. */ + private void updateRegistration (Resource obj, JsonValue config) + { + if (!(config instanceof JsonObject)) + throw new Err.BadJson(config); + var spec = (JsonObject)config; + + if (!spec.getBoolean("strict")) { + log.info("Objects must be strict"); + throw new Err.BadJson(spec.get("strict")); + } + if (!spec.getString("owner").equals(Vocab.U_Unowned.toString())) { + log.info("Owners not implemented yet"); + throw new Err.Forbidden(); + } + + var rank = db.findRank(obj); + if (spec.getInt("rank") != rank) { + log.info("Rank changes can only be made via dumps"); + throw new Err.RankMismatch(); + } + + var model = db.derived(); + var uuid = Util.single(model.listObjectsOfProperty(obj, Vocab.uuid)) + .map(AppMapper::literalToJson) + .orElseThrow(() -> new Err.CorruptRDF("Cannot find object UUID")); + if (!spec.get("uuid").equals(uuid)) { + log.info("UUIDs cannot be changed: {} vs {}", uuid, spec.get("uuid")); + throw new Err.BadJson(spec.get("uuid")); + } + + var klass = Vocab.parseUUID(spec.getString("class")) + .flatMap(db::findObject) + .map(FPObject::node) + .orElseThrow(() -> { + log.info("Cannot find new primary class"); + return new Err.BadJson(spec.get("class")); + }); + /* This is a change from the JS implementation; here we require + * the object to already be a member of the new primary class. + * The ConfigDB will create a new direct membership if needed, + * but will not remove it again if the primary class changes. + * This is inconsistent and so has been changed. */ + if (!model.contains(obj, RDF.type, klass)) { + log.info("Object not a member of new primary class"); + throw new Err.NotMember(); + } + model.removeAll(obj, Vocab.primary, null); + model.add(obj, Vocab.primary, klass); + + model.removeAll(obj, Vocab.deleted, null); + model.addLiteral(obj, Vocab.deleted, spec.getBoolean("deleted")); + } + + /* Currently this does dynamic decoding based on what was returned + * from the RDF. Once we have query generation we should be able to + * use the properties to know what we are expecting. */ + private static JsonValue literalToJson (RDFNode node) + { + /* This should probably be selected based on optional/nullable + * properties in the schema. */ + if (node == null) return JsonValue.NULL; + + if (!node.isLiteral()) + throw new Err.NotLiteral(node); + + var lit = node.asLiteral(); + var typ = ResourceFactory.createResource(lit.getDatatypeURI()); + + return Optional.ofNullable(toJson.get(typ)) + .flatMap(d -> d.apply(lit.getLexicalForm())) + .orElseThrow(() -> new Err.BadLiteral(lit)); + } + + private static JsonValue solutionToJson (QuerySolution rs) + { + var jobj = Json.createObjectBuilder(); + rs.varNames().forEachRemaining(n -> + jobj.add(n, literalToJson(rs.get(n)))); + + return jobj.build(); + } + + /* XXX This can only convert strings for now. In general we will + * need expected type information as RDF has a richer type system + * than JSON. */ + private static Literal jsonToLiteral (JsonValue val) + { + if (!(val instanceof JsonString)) + throw new Err.BadJson(val); + + return ResourceFactory.createPlainLiteral( + ((JsonString)val).getString()); + } + + /* This can only decode flat objects for now */ + private static QuerySolution jsonToSolution (JsonValue val) + { + if (!(val instanceof JsonObject)) + throw new Err.BadJson(val); + + var qsol = new QuerySolutionMap(); + ((JsonObject)val).forEach( + (k, v) -> qsol.add(k, jsonToLiteral(v))); + + return qsol; + } +} diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/AppUpdater.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/AppUpdater.java new file mode 100644 index 00000000..5976b4d4 --- /dev/null +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/AppUpdater.java @@ -0,0 +1,133 @@ +/* + * Factory+ metadata database + * Structured app update processor + * Copyright 2026 University of Sheffield AMRC + */ + +package uk.co.amrc.factoryplus.metadb.db; + +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Function; + +import jakarta.json.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.jena.query.*; +import org.apache.jena.rdf.model.*; +import org.apache.jena.vocabulary.*; + +public class AppUpdater extends RequestHandler.Component +{ + private static final Logger log = LoggerFactory.getLogger(AppUpdater.class); + + private record Config (Resource app, Resource obj) {} + + private ObjectStructure objs; + private AppMapper mapper; + private Set updated; + + public AppUpdater (RequestHandler req) + { + super(req); + this.objs = request().objectStructure(); + this.mapper = db().appMapper(); + this.updated = new HashSet<>(); + } + + private static final Query Q_findUpdates = Vocab.query(""" + select ?app ?obj + where { + ?app a ; + ?domain. + ?obj a ?domain; + ?uuid. + + # We do not generate Reg entries here, they must be done in + # a second pass. + filter(?app != ) + } + """); + private static Query Q_findObjects = Vocab.query(""" + select ?obj + where { + ?obj ?uuid. + + # We don't generate Reg entries for Reg entries. + filter not exists { + ?obj a . + } + } + """); + /* This query assumes we do not have sub-apps, so that a given + * ConfigEntry is a member of only one App. If SPARQL had DELETE + * RETURNING this could be an UpdateRequest: we must know what was + * removed to send notify updates. + */ + private static Query Q_orphanConfigs = Vocab.query(""" + select ?conf ?app ?obj + where { + ?conf a ?app; ?obj. + ?app ?domain. + + filter not exists { ?obj a ?domain. } + } + """); + + public void update () + { + var updates = db().selectQuery(Q_findUpdates) + .materialise(); + updates.forEachRemaining(row -> { + var app = row.getResource("app"); + var obj = row.getResource("obj"); + updateEntry(app, obj); + }); + + /* We update Reg entries in a second pass, as updating the other + * entries will have created registered objects. */ + var objects = db().selectQuery(Q_findObjects) + .materialise(); + objects.forEachRemaining(row -> { + var obj = row.getResource("obj"); + updateEntry(Vocab.App.Registration, obj); + }); + + var orphans = db().selectQuery(Q_orphanConfigs) + .materialise(); + orphans.forEachRemaining(row -> { + var conf = row.getResource("conf"); + db().removeResource(conf); + var app = row.getResource("app"); + var obj = row.getResource("obj"); + updated.add(new Config(app, obj)); + }); + } + + public void publish () + { + log.info("Config updates:"); + for (var c : updated) + log.info(" {} {}", c.app(), c.obj()); + } + + private void updateEntry (Resource app, Resource obj) + { + /* XXX We need to remove redundant entries */ + + log.info("Updating {} {}", app, obj); + var changed = mapper.generateConfig(app, obj) + .map(v -> request().configEntry(app, obj).putRawValue(v)) + .orElse(false); + + if (changed) + updated.add(new Config(app, obj)); + } + +} diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/ConfigEntry.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/ConfigEntry.java index fb74c969..30f6b84f 100644 --- a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/ConfigEntry.java +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/ConfigEntry.java @@ -21,53 +21,56 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ConfigEntry extends RequestHandler +public class ConfigEntry extends RequestHandler.Component { private static final Logger log = LoggerFactory.getLogger(ConfigEntry.class); private Resource app; private Resource obj; - public ConfigEntry (RdfStore db, Resource app, Resource obj) + public ConfigEntry (RequestHandler req, Resource app, Resource obj) { - super(db); + super(req); this.app = app; this.obj = obj; } - public static ConfigEntry create (RdfStore db, UUID app, UUID obj) + public static ConfigEntry create (RequestHandler req, UUID app, UUID obj) { - var appO = db.findObjectOrError(app); - var objO = db.findObjectOrError(obj); + var appO = req.db().findObjectOrError(app); + var objO = req.db().findObjectOrError(obj); - return new ConfigEntry(db, appO.node(), objO.node()); + return new ConfigEntry(req, appO.node(), objO.node()); } - public record Value (JsonValue value, String etag, Instant mtime) {} + public record Value (JsonValue value, String etag, Optional mtime) {} + + private boolean isStructured () + { + return db().derived().contains(app, RDF.type, Vocab.App.Structured); + } private static final Query Q_getValue = Vocab.query(""" - select ?value ?etag ?mtime + select ?value ?etag where { ?config a ?app; ?obj; - ?value. - ?config ?instant. - ?instant ?etag; - ?mtime. + ?etag; + ?value. } """); public Optional getValue () { - return db.optionalQuery(Q_getValue, "app", app, "obj", obj) + return db().optionalQuery(Q_getValue, "app", app, "obj", obj) .map(binding -> { var val = Util.decodeLiteral(binding.get("value"), RDF.JSON, s -> Json.createReader(new StringReader(s)).readValue()); var etag = Util.decodeLiteral(binding.get("etag"), XSD.xstring, s -> s); - var mtime = Util.decodeLiteral(binding.get("mtime"), XSD.dateTime, - Instant::parse); + //var mtime = Util.decodeLiteral(binding.get("mtime"), XSD.dateTime, + // Instant::parse); - return new Value(val, etag, mtime); + return new Value(val, etag, Optional.empty()); }); } @@ -86,24 +89,49 @@ public Optional getValue () public void removeValue () { - db.runUpdate(U_removeValue, "app", app, "obj", obj); + if (isStructured()) + db().appMapper().deleteConfig(app, obj); + else + removeRawValue(); + } + + public void removeRawValue () + { + db().runUpdate(U_removeValue, "app", app, "obj", obj); } public void putValue (JsonValue value) { - removeValue(); + if (isStructured()) + db().appMapper().updateConfig(app, obj, value); + else + putRawValue(value); + } + + public boolean putRawValue (JsonValue value) + { + var existing = getValue() + .map(Value::value) + .filter(v -> v.equals(value)); + if (existing.isPresent()) { + log.info("Duplicate config update suppressed"); + return false; + } + + removeRawValue(); var json = ResourceFactory.createTypedLiteral( value.toString(), RDF.dtRDFJSON); - var graph = db.derived(); - var entry = graph.createResource(); - var inst = db.createInstant(); + var graph = db().derived(); + var entry = db().createObject(app).node(); + //var inst = request().getInstant(); + + graph.add(entry, Vocab.App.forP, obj); + graph.add(entry, Vocab.Doc.content, json); + //graph.add(entry, Vocab.Time.start, inst); - graph.add(entry, RDF.type, app); - graph.add(entry, Vocab.forP, obj); - graph.add(entry, Vocab.value, json); - graph.add(entry, Vocab.start, inst); + return true; } } diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/Err.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/Err.java index 986b7270..e774c849 100644 --- a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/Err.java +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/Err.java @@ -8,6 +8,10 @@ import java.util.UUID; +import jakarta.json.*; + +import org.apache.jena.rdf.model.*; + public class Err extends Error { public Err (String msg) @@ -37,6 +41,20 @@ public Config (String msg, UUID app, UUID obj) public UUID app () { return app; } public UUID obj () { return obj; } } + public static class NotLiteral extends CorruptRDF + { + public NotLiteral (RDFNode n) + { + super("Expecting literal, not " + n.toString()); + } + } + public static class BadLiteral extends CorruptRDF + { + public BadLiteral (Literal l) + { + super("Cannot decode literal: " + l.toString()); + } + } public static abstract class ClientError extends Err { @@ -47,6 +65,15 @@ public ClientError (String msg) public abstract int statusCode (); } + public static class Forbidden extends ClientError + { + public Forbidden () + { + super("Access denied"); + } + + public int statusCode () { return 403; } + } public static class NotFound extends ClientError { public NotFound (String res) @@ -65,31 +92,33 @@ public InvalidName (String name) public int statusCode () { return 410; } } - public static class RankMismatch extends ClientError + public static class BadJson extends ClientError { - public RankMismatch () + public BadJson (JsonValue val) { - super("Rank mismatch"); + super("Unexpected JSON value: " + val.toString()); } + public int statusCode () { return 422; } + } + public static class RankMismatch extends ClientError + { + public RankMismatch () { super("Rank mismatch"); } + public int statusCode () { return 409; } + } + public static class NotMember extends ClientError + { + public NotMember () { super("Not a member of a required class"); } public int statusCode () { return 409; } } public static class InUse extends ClientError { - public InUse () - { - super("Object in use"); - } - + public InUse () { super("Object in use"); } public int statusCode () { return 409; } } public static class Immutable extends ClientError { - public Immutable () - { - super("Object is immutable"); - } - + public Immutable () { super("Object is immutable"); } public int statusCode () { return 405; } } } diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/NotifyListener.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/NotifyListener.java new file mode 100644 index 00000000..cfd61b8e --- /dev/null +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/NotifyListener.java @@ -0,0 +1,111 @@ +/* + * Factory+ metadata database + * Change-notify model listener + * Copyright 2026 University of Sheffield AMRC + */ + +package uk.co.amrc.factoryplus.metadb.db; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.jena.rdf.listeners.StatementListener; +import org.apache.jena.rdf.model.*; +import org.apache.jena.vocabulary.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class NotifyListener extends StatementListener +{ + /* Currently (with TDB2) it appears that Jena will only allow one + * write transaction to be active at a time. A subsequent parallel + * txn will block on the first write until the first commits or + * aborts. We rely on this to track which changes belong to which + * txn, and whether they committed or not. If this changes this + * class will need to track thread IDs and rely on the + * one-txn-per-thread restriction instead. */ + + /* Jena ModelChangedListeners do not appear to respect inference + * when reporting additions, but do when reporting removals. This is + * surely a bug but one we have to work with for now. For + * consistency we watch the direct model, and rely on the derived + * model not making use of any RDFS except rdfs:subClassOf. In + * particular: + * - No use of rdfs:subPropertyOf. + * - No use of rdfs:{domain,range} to infer rdf:type. + * + * We can optimise the class structure handling by notifying the + * rank of the changed class. With strict ranks it is not possible + * for a change in one rank to propagate into other ranks. If we + * introduced inference for powertypes this would change. + * + * Subproperties could probably be handled by maintaining a cache of + * the subproperty tree, and firing derived statements by hand. This + * might be difficult when removing statements as we won't know + * whether the derived statement is still present via another route. + * As long as our interface is 'there may have been a change in this + * property' this won't matter. Subproperties of rdf:type/ + * rdfs:subClassOf are likely to be the most important case, and + * could perhaps be handled specially. + * + * Handling derived config entries will be more difficult, even with + * inference restricted to the class structure. Probably it will be + * necessary to be rather crude and maintain a list of 'these + * predicates might affect this Application' and check all entries + * when any change. Restricting the domain of the Application may be + * important here to avoid needing to check every F+ object. + */ + + private record Config (Resource app, Resource obj) {} + + private static final Logger log = LoggerFactory.getLogger(NotifyListener.class); + + private boolean done = false; + private Set changed = new HashSet<>(); + + private void checkDone () + { + if (done) + throw new IllegalStateException("Listener is complete"); + } + + public void commit () + { + checkDone(); + done = true; + } + public void abort () + { + checkDone(); + changed = Set.of(); + done = true; + } + public void end () + { + if (!done) + throw new IllegalStateException("Listener is not complete"); + log.info("CHANGED:"); + for (var p: changed) + log.info(" {}", p); + } + + private void changed (Statement stmt) + { + checkDone(); + changed.add(stmt.getPredicate()); + } + + public void addedStatement (Statement stmt) + { + log.info("ADDED\n {}", stmt); + changed(stmt); + } + + public void removedStatement (Statement stmt) + { + log.info("REMOVED\n {}", stmt); + changed(stmt); + } +} + diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/ObjectStructure.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/ObjectStructure.java index da3434e0..201dbca0 100644 --- a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/ObjectStructure.java +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/ObjectStructure.java @@ -23,18 +23,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ObjectStructure extends RequestHandler +public class ObjectStructure extends RequestHandler.Component { private static final Logger log = LoggerFactory.getLogger(ObjectStructure.class); - private ObjectStructure (RdfStore db) + private ObjectStructure (RequestHandler req) { - super(db); + super(req); } - public static ObjectStructure create (RdfStore db) + public static ObjectStructure create (RequestHandler req) { - return new ObjectStructure(db); + return new ObjectStructure(req); } /* XXX Ideally both of these switch statements would become @@ -53,15 +53,15 @@ public static Relation of (String relation) } private static final Set IMMUTABLE = Set.of( - Vocab.Application, Vocab.Special, - Vocab.Registration, Vocab.ConfigSchema, + Vocab.App.Application, Vocab.Special, + Vocab.App.Registration, Vocab.App.ConfigSchema, Vocab.Wildcard, Vocab.Unowned); private Model findGraph (String name) { switch (name) { - case "direct": return db.direct(); - case "derived": return db.derived(); + case "direct": return db().direct(); + case "derived": return db().derived(); default: throw new Err.NotFound("No such graph"); } } @@ -69,12 +69,11 @@ private Model findGraph (String name) /* These could return UUIDs, but it seems silly. */ public List listObjects () { - return db.calculateRead(() -> - db.derived() - .listObjectsOfProperty(Vocab.uuid) - .filterKeep(n -> n.isLiteral()) - .mapWith(o -> o.asLiteral().getString()) - .toList()); + return db().derived() + .listObjectsOfProperty(Vocab.uuid) + .filterKeep(n -> n.isLiteral()) + .mapWith(o -> o.asLiteral().getString()) + .toList(); } private static final Query Q_listRanks = Vocab.query(""" @@ -88,41 +87,10 @@ order by asc(?rank) public List listRanks () { - return db.calculateRead(() -> { - var rs = db.selectQuery(Q_listRanks); - return Iterator.ofAll(rs) - .map(s -> s.getLiteral("uuid").getString()) - .toJavaList(); - }); - } - - private static final Query Q_objectRegistration = Vocab.query(""" - select ?uuid ?rank ?class - where { - ?obj ?uuid; - rdf:type/ ?rank; - / ?class. - } - """); - - /* TXN */ - private JsonValue objectRegistration (FPObject obj) - { - var rs = db.singleQuery(Q_objectRegistration, "obj", obj.node()); - - String uuid = Util.decodeLiteral(rs.get("uuid"), XSD.xstring, s -> s); - String klass = Util.decodeLiteral(rs.get("class"), XSD.xstring, s -> s); - int rank = Util.decodeLiteral(rs.get("rank"), XSD.xint, Integer::valueOf); - - return Json.createObjectBuilder() - .add("uuid", uuid) - .add("rank", rank) - .add("class", klass) - /* These entries are fake, for now */ - .add("owner", Vocab.U_Unowned.toString()) - .add("strict", true) - .add("deleted", false) - .build(); + var rs = db().selectQuery(Q_listRanks); + return Iterator.ofAll(rs) + .map(s -> s.getLiteral("uuid").getString()) + .toJavaList(); } /* TXN */ @@ -130,13 +98,13 @@ private FPObject updateRegistration (FPObject obj, Resource klass) { log.info("Update registration: {}, {}", obj, klass); - var orank = db.findRank(obj.node()); - var krank = db.findRank(klass); + var orank = db().findRank(obj.node()); + var krank = db().findRank(klass); if (krank != orank + 1) throw new Err.RankMismatch(); - var model = db.derived(); + var model = db().derived(); model.removeAll(obj.node(), Vocab.primary, null); model.add(obj.node(), Vocab.primary, klass); if (!model.contains(obj.node(), RDF.type, klass)) @@ -156,18 +124,18 @@ private FPObject updateRegistration (FPObject obj, Resource klass) * a Jakarta Response, which is a clear layer violation. */ public JsonValue createObject (UUID klass, Optional uuid) { - return db.calculateWrite(() -> { - var kres = db.findObjectOrError(klass).node(); - - var obj = uuid - .map(u -> db.findObject(u) - .map(o -> { log.info("Found object {}", o); return o; }) - .map(o -> updateRegistration(o, kres)) - .orElseGet(() -> db.createObject(kres, u))) - .orElseGet(() -> db.createObject(kres)); - - return objectRegistration(obj); - }); + var kres = db().findObjectOrError(klass).node(); + + var obj = uuid + .map(u -> db().findObject(u) + .map(o -> { log.info("Found object {}", o); return o; }) + .map(o -> updateRegistration(o, kres)) + .orElseGet(() -> db().createObject(kres, u))) + .orElseGet(() -> db().createObject(kres)); + + return db().appMapper() + .generateConfig(Vocab.App.Registration, obj.node()) + .orElseThrow(() -> new Err.CorruptRDF("Cannot find object registration")); } private static UpdateRequest U_deleteConfigs = Vocab.update(""" @@ -182,39 +150,37 @@ public JsonValue createObject (UUID klass, Optional uuid) public void deleteObject (UUID uuid) { - db.executeWrite(() -> { - log.info("Delete object {}", uuid); - var obj = db.findObjectOrError(uuid).node(); - log.info("Found node {}", obj); - if (IMMUTABLE.contains(obj)) { - log.info("Object {} is immutable", obj); - throw new Err.Immutable(); - } + log.info("Delete object {}", uuid); + var obj = db().findObjectOrError(uuid).node(); + log.info("Found node {}", obj); + if (IMMUTABLE.contains(obj)) { + log.info("Object {} is immutable", obj); + throw new Err.Immutable(); + } - /* We only check for direct dependents. If we have no direct - * dependents we should also have no indirect. This relies - * on no use of rdfs:domain etc. to infer memberships. */ - var model = db.direct(); - - /* This will also catch configs-of-app. If we want to return - * UUIDs in the 409 we will need to handle these separately. */ - var members = model.listResourcesWithProperty(RDF.type, obj); - if (members.hasNext()) { - log.info("Object {} has members:", uuid); - members.forEachRemaining(m -> log.info(" {}", m)); - throw new Err.InUse(); - } + /* We only check for direct dependents. If we have no direct + * dependents we should also have no indirect. This relies + * on no use of rdfs:domain etc. to infer memberships. */ + var model = db().direct(); + + /* This will also catch configs-of-app. If we want to return + * UUIDs in the 409 we will need to handle these separately. */ + var members = model.listResourcesWithProperty(RDF.type, obj); + if (members.hasNext()) { + log.info("Object {} has members:", uuid); + members.forEachRemaining(m -> log.info(" {}", m)); + throw new Err.InUse(); + } - var subclasses = model.listResourcesWithProperty(RDFS.subClassOf, obj); - if (subclasses.hasNext()) { - log.info("Object {} has subclasses:", uuid); - subclasses.forEachRemaining(m -> log.info(" {}", m)); - throw new Err.InUse(); - } + var subclasses = model.listResourcesWithProperty(RDFS.subClassOf, obj); + if (subclasses.hasNext()) { + log.info("Object {} has subclasses:", uuid); + subclasses.forEachRemaining(m -> log.info(" {}", m)); + throw new Err.InUse(); + } - db.runUpdate(U_deleteConfigs, "obj", obj); - model.removeAll(obj, null, null); - }); + db().runUpdate(U_deleteConfigs, "obj", obj); + model.removeAll(obj, null, null); } public List listRelation (String gname, UUID uuid, String relation) @@ -222,15 +188,13 @@ public List listRelation (String gname, UUID uuid, String relation) var graph = findGraph(gname); var rel = Relation.of(relation); - return db.calculateRead(() -> { - var klass = db.findObjectOrError(uuid); - return graph - .listResourcesWithProperty(rel.prop(), klass.node()) - .mapWith(r -> r.getProperty(Vocab.uuid)) - .filterKeep(s -> s != null) - .mapWith(s -> s.getString()) - .toList(); - }); + var klass = db().findObjectOrError(uuid); + return graph + .listResourcesWithProperty(rel.prop(), klass.node()) + .mapWith(r -> r.getProperty(Vocab.uuid)) + .filterKeep(s -> s != null) + .mapWith(s -> s.getString()) + .toList(); } public boolean testRelation (String gname, UUID klass, String relation, UUID object) @@ -238,40 +202,34 @@ public boolean testRelation (String gname, UUID klass, String relation, UUID obj var graph = findGraph(gname); var rel = Relation.of(relation); - return db.calculateRead(() -> { - var kres = db.findObjectOrError(klass).node(); - var ores = db.findObjectOrError(object).node(); - /* We cannot use methods on ores as this is always from the - * direct graph. */ - return !graph.listStatements(ores, rel.prop(), kres) - .toList() - .isEmpty(); - }); + var kres = db().findObjectOrError(klass).node(); + var ores = db().findObjectOrError(object).node(); + /* We cannot use methods on ores as this is always from the + * direct graph. */ + return !graph.listStatements(ores, rel.prop(), kres) + .toList() + .isEmpty(); } public void putRelation (UUID klass, String relation, UUID object) { var rel = Relation.of(relation); - db.executeWrite(() -> { - var kres = db.findObjectOrError(klass).node(); - var ores = db.findObjectOrError(object).node(); + var kres = db().findObjectOrError(klass).node(); + var ores = db().findObjectOrError(object).node(); - var krank = db.findRank(kres); - var orank = db.findRank(ores); - if (krank != orank + rel.offset()) - throw new Err.RankMismatch(); + var krank = db().findRank(kres); + var orank = db().findRank(ores); + if (krank != orank + rel.offset()) + throw new Err.RankMismatch(); - db.direct().add(ores, rel.prop(), kres); - }); + db().direct().add(ores, rel.prop(), kres); } public void delRelation (UUID klass, String relation, UUID object) { var rel = Relation.of(relation); - db.executeWrite(() -> { - var kres = db.findObjectOrError(klass).node(); - var ores = db.findObjectOrError(object).node(); - db.direct().remove(ores, rel.prop(), kres); - }); + var kres = db().findObjectOrError(klass).node(); + var ores = db().findObjectOrError(object).node(); + db().direct().remove(ores, rel.prop(), kres); } } diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/RdfStore.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/RdfStore.java index cc30c730..cf1e2e55 100644 --- a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/RdfStore.java +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/RdfStore.java @@ -7,6 +7,8 @@ package uk.co.amrc.factoryplus.metadb.db; import java.time.Instant; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import java.util.Map; import java.util.Optional; @@ -38,6 +40,8 @@ public class RdfStore private InfModel derived; private Dataset dataset; + private AppMapper appMapper; + /* We build a Dataset out of these named graphs: * - G_direct: this is G_direct from the TDB. * - G_derived: this is RDFS(G_direct). @@ -52,17 +56,45 @@ public RdfStore (String data) dataset.addNamedModel(Vocab.G_direct, direct); dataset.addNamedModel(Vocab.G_derived, derived); + + appMapper = new AppMapper(this); } public Dataset dataset () { return dataset; } public Model direct () { return direct; } public InfModel derived () { return derived; } + public AppMapper appMapper () { return appMapper; } + public void executeRead (Runnable r) { dataset.executeRead(r); } - public void executeWrite (Runnable r) { dataset.executeWrite(r); } public T calculateRead (Supplier s) { return dataset.calculateRead(s); } + + public void executeWrite (Runnable r) { dataset.executeWrite(r); } public T calculateWrite (Supplier s) { return dataset.calculateWrite(s); } + public T requestRead (Function cb) + { + return calculateRead(() -> cb.apply(new RequestHandler(this))); + } + public T requestWrite (Function cb) + { + var req = new RequestHandler(this); + var updater = req.appUpdater(); + + T rv = calculateWrite(() -> { + var rv2 = cb.apply(req); + updater.update(); + return rv2; + }); + + updater.publish(); + return rv; + } + public void requestExecute (Consumer cb) + { + requestWrite(req -> { cb.accept(req); return 1; }); + } + public ResultSet selectQuery (Query query, Object... substs) { var exec = QueryExecution.dataset(dataset) @@ -100,7 +132,16 @@ public Optional findResource (Property pred, RDFNode obj) direct.listResourcesWithProperty(pred, obj)); } - /* TXN */ + public void removeResource (Resource node) + { + derived.removeAll(node, null, null); + derived.removeAll(null, null, node); + + /* We don't do this yet but we may in the future. */ + if (node.canAs(Property.class)) + derived.removeAll(null, node.as(Property.class), null); + } + public Optional findObject (UUID uuid) { return findResource(Vocab.uuid, Vocab.uuidLiteral(uuid)) @@ -130,7 +171,6 @@ public int findRank (Resource obj) return Util.decodeLiteral(binding.get("rank"), XSD.xint, Integer::parseInt); } - /* TXN */ public FPObject createObject (Resource klass) { UUID uuid; @@ -144,10 +184,9 @@ public FPObject createObject (Resource klass) return createObject(klass, uuid); } - /* TXN */ public FPObject createObject (Resource klass, UUID uuid) { - var obj = derived.createResource(); + var obj = Vocab.uuidResource(uuid); derived.add(obj, Vocab.uuid, uuid.toString()); derived.add(obj, RDF.type, klass); derived.add(obj, Vocab.primary, klass); @@ -161,22 +200,12 @@ public FPObject createObject (Resource klass, UUID uuid) return new FPObject(obj, uuid); } - /* TXN */ public Resource createInstant () { - var inst = createObject(Vocab.Instant).node(); + var inst = createObject(Vocab.Time.Instant).node(); var stamp = derived.createTypedLiteral(Instant.now(), XSDDatatype.XSDdateTime); - derived.add(inst, Vocab.timestamp, stamp); + derived.add(inst, Vocab.Time.timestamp, stamp); return inst; } - public ObjectStructure objectStructure () - { - return ObjectStructure.create(this); - } - - public ConfigEntry configEntry (UUID app, UUID obj) - { - return ConfigEntry.create(this, app, obj); - } } diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/RequestHandler.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/RequestHandler.java index 8b1902cc..16104af8 100644 --- a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/RequestHandler.java +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/RequestHandler.java @@ -6,16 +6,59 @@ package uk.co.amrc.factoryplus.metadb.db; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.apache.jena.rdf.model.*; + +import io.vavr.Lazy; + /* Eventually these objects will need to be associated with a txn, and a * request, and contain the request UPN and the current txn Instant and * other per-request information. Possibly at this point this will * become a second object rather than a superclass. */ -public abstract class RequestHandler +public class RequestHandler { - protected RdfStore db; + public static abstract class Component + { + private RequestHandler req; + + protected Component (RequestHandler req) + { + this.req = req; + } - protected RequestHandler (RdfStore db) + protected RequestHandler request () { return req; } + protected RdfStore db () { return req.db(); } + } + + private RdfStore db; + private Lazy now; + + public RequestHandler (RdfStore db) { this.db = db; + this.now = Lazy.of(db::createInstant); + } + + public RdfStore db () { return db; } + public Resource getInstant () { return now.get(); } + + public ObjectStructure objectStructure () + { + return ObjectStructure.create(this); + } + public ConfigEntry configEntry (UUID app, UUID obj) + { + return ConfigEntry.create(this, app, obj); + } + public ConfigEntry configEntry (Resource app, Resource obj) + { + return new ConfigEntry(this, app, obj); + } + public AppUpdater appUpdater () + { + return new AppUpdater(this); } } diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/Util.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/Util.java index 89a434a4..bd2a400b 100644 --- a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/Util.java +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/Util.java @@ -6,23 +6,45 @@ package uk.co.amrc.factoryplus.metadb.db; +import java.io.StringReader; import java.util.Iterator; import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; + +import jakarta.json.*; import org.apache.jena.query.ResultSet; import org.apache.jena.rdf.model.*; import org.apache.jena.util.iterator.ClosableIterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.vavr.*; import io.vavr.control.*; final class Util { + private static final Logger log = LoggerFactory.getLogger(Util.class); + private static Throwable error (String fmt, Object... args) { String msg = String.format(fmt, args); return new Err.CorruptRDF(msg); } + /* This will silently ignore trailing garbage. I think this could be + * cured by using JsonParser instead but it's not straightforward. */ + public static Optional readJson (String json) + { + var sr = new StringReader(json); + var jr = Json.createReader(sr); + + return Try.of(jr::readValue) + .andFinally(jr::close) + .toJavaOptional(); + } + public static T decodeLiteral (RDFNode node, Resource type, CheckedFunction1 extract) { @@ -46,11 +68,15 @@ public static Optional single (Iterator it) var rv = it.next(); if (it.hasNext()) { - if (it instanceof ClosableIterator) - ((ClosableIterator)it).close(); - /* Annoying failure to implement the appropriate interface… */ - if (it instanceof ResultSet) - ((ResultSet)it).close(); +// if (it instanceof ClosableIterator) +// ((ClosableIterator)it).close(); +// /* Annoying failure to implement the appropriate interface… */ +// if (it instanceof ResultSet) +// ((ResultSet)it).close(); + + /* rather than closing early, log the results */ + log.info("Expected single result: {}", rv); + it.forEachRemaining(v -> log.info("Unexpected extra result: {}", v)); throw new Err.CorruptRDF("Expected single result, found multiple"); } diff --git a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/Vocab.java b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/Vocab.java index f8ba18a7..f80ad5cc 100644 --- a/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/Vocab.java +++ b/acs-metadb/src/main/java/uk/co/amrc/factoryplus/metadb/db/Vocab.java @@ -36,20 +36,36 @@ public static Property prop (String p) { public static final Property uuid = prop("core/uuid"); public static final Property rank = prop("core/rank"); public static final Property primary = prop("core/primary"); - public static final Property start = prop("core/start"); + public static final Property deleted = prop("core/deleted"); public static final Resource Special = res("core/Special"); public static final Resource Wildcard = res("core/Wildcard"); public static final Resource Unowned = res("core/Unowned"); - public static final Resource Instant = res("core/Instant"); - public static final Property timestamp = prop("core/timestamp"); + public static class Time { + public static final Resource Instant = res("time/Instant"); - public static final Resource Application = res("core/Application"); - public static final Property forP = prop("app/for"); - public static final Property value = prop("app/value"); - public static final Resource Registration = res("app/Registration"); - public static final Resource ConfigSchema = res("app/ConfigSchema"); + public static final Property start = prop("time/start"); + public static final Property timestamp = prop("time/timestamp"); + } + + public static class Doc { + public static final Resource Document = res("doc/Document"); + + public static final Property content = prop("doc/content"); + } + + public static class App { + public static final Resource Application = res("app/Application"); + public static final Resource Structured = res("app/Structured"); + public static final Resource Registration = res("app/Registration"); + public static final Resource ConfigSchema = res("app/ConfigSchema"); + public static final Resource Info = res("app/Info"); + public static final Resource SparkplugAddr = res("app/SparkplugAddress"); + + public static final Property forP = prop("app/for"); + public static final Property value = prop("app/value"); + } public static final Resource G_direct = res("graph/direct"); public static final Resource G_derived = res("graph/derived"); diff --git a/acs-metadb/ttl/core.ttl b/acs-metadb/ttl/core.ttl index acf4b15b..32b9ae97 100644 --- a/acs-metadb/ttl/core.ttl +++ b/acs-metadb/ttl/core.ttl @@ -6,8 +6,6 @@ @base . @prefix uuid: . @prefix : . -@prefix app: . -@prefix srv: . # These are proper classes and sit outside the rank structure. :Object a rdfs:Class. @@ -23,6 +21,9 @@ :uuid :from :Object; :to xsd:string. :rank :from :Object; :to xsd:integer. +# This should become some form of name-in-naming-scheme. +:name :from :Object; :to xsd:string. + # Factory+ primary class. This is not a subproperty of rdf:type as we # need to track all memberships in the direct graph. Enforcement that # the primary class is actually a class happens in the API. @@ -31,71 +32,102 @@ # This is purely informational at the moment. :powersetOf :from :Class; :to :Class. -## Implementation of subsets in terms of powersets. -## These need to be forward rules, I think. -# a ∈ b, b ⊂ c ⇒ a ∈ c -# a ∈ b, b ∈ p, p ℙ c ⇒ a ∈ c +# Implementation of subsets in terms of powersets. +# These need to be forward rules, I think. +# +## a ∈ b, b ⊂ c ⇒ a ∈ c +## a ∈ b, b ∈ p, p ℙ c ⇒ a ∈ c # -# x ⊂ y, y ⊂ z ⇒ x ⊂ z -# x ∈ m, m ℙ y, y ∈ n, n ℙ z ⇒ x ∈ r, r ℙ z -## r = n as powersets are unique -# x ∈ m, m ℙ y, y ∈ n, n ℙ z ⇒ x ∈ n -## z is not needed -# x ∈ m, m ℙ y, y ∈ n ⇒ x ∈ n +## x ⊂ y, y ⊂ z ⇒ x ⊂ z +## x ∈ m, m ℙ y, y ∈ n, n ℙ z ⇒ x ∈ r, r ℙ z +# r = n as powersets are unique +## x ∈ m, m ℙ y, y ∈ n, n ℙ z ⇒ x ∈ n # -## then also (backward rules?): -# p ℙ a ⇒ a ∈ p -# a ∈ p, p ℙ b ⇒ a ⊂ p -# a R b, R ∈ p, p ℙ S ⇒ a S b +# We cannot remove the condition n ℙ z without adding an assertion that +# n is a powerset, e.g. n ∈ PowerSet where PowerSet is a proper class. +# Without this we might have e.g. n = { y } and so cannot derive x ∈ n. +# Possibly we could replace m ℙ x with m ∈ PowerSet, x ∈ m throughout, +# and instead of referencing 'the powerset of x', reference 'a powerset +# containing x', where a powerset m obeys +## x ∈ m, (∀a: a ∈ y ⇒ a ∈ x) ⇒ y ∈ m +# +# then also (backward rules?): +## p ℙ a ⇒ a ∈ p +## a ∈ p, p ℙ b ⇒ a ⊂ p +## a R b, R ∈ p, p ℙ S ⇒ a S b :Individual a :R1Class; :primary :R1Class; rdfs:subClassOf :Object; :uuid "2494ae9b-cd87-4c01-98db-437a303b43e9"; + :name "Individual"; :rank "0"^^xsd:int. :R1Class a :R2Class; :primary :R2Class; rdfs:subClassOf :Class; :uuid "04a1c90d-2295-4cbe-b33a-74eded62cbf1"; + :name "Rank 1 class"; :rank "1"^^xsd:int; :powersetOf :Individual. :R2Class a :R3Class; :primary :R3Class; rdfs:subClassOf :Class; :uuid "705888ce-53fa-434d-afee-274b331d4642"; + :name "Rank 2 class"; :rank "2"^^xsd:int; :powersetOf :R1Class. # The top rank class has no primary class :R3Class a :Class; rdfs:subClassOf :Class; :uuid "52b80183-6998-4bf9-9b30-132755e7dede"; + :name "Rank 3 class"; :rank "3"^^xsd:int; :powersetOf :R2Class. # XXX These are definitely not R0. They are perhaps names, which are R1? :Special a :R1Class; :primary :R1Class; rdfs:subClassOf :Individual; - :uuid "ddb132e4-5cdd-49c8-b9b1-2f35879eab6d". + :uuid "ddb132e4-5cdd-49c8-b9b1-2f35879eab6d"; + :name "Special UUID". # There is some logic in saying this should be :Object. :Wildcard a :Special; :primary :Special; - :uuid "00000000-0000-0000-0000-000000000000". -# This makes no logical sense. We should omit the owner instead. + :uuid "00000000-0000-0000-0000-000000000000"; + :name "Wildcard". +# This makes no logical sense. We should omit the owner instead, or +# replace the owner link with a link to the set of objects belonging to +# that owner, where the Unowned set has no owner reference. :Unowned a :Special; :primary :Special; - :uuid "091e796a-65c0-4080-adff-c3ce01a65b2e". + :uuid "091e796a-65c0-4080-adff-c3ce01a65b2e"; + :name "Unowned". -:Instant a :R1Class; :primary :R1Class; +