From b992861e2e46ceb2f53006b81bcf5212d518511a Mon Sep 17 00:00:00 2001 From: Ian Patterson Date: Wed, 17 Jun 2026 14:56:03 -0500 Subject: [PATCH 1/4] feat(consys): add custom obs/command format hooks for data and served schemas Extend the CustomObsFormat SPI with command-side hooks (getCommandBinding, getCommandSchemaBinding) and an isAutoSelectable flag, and thread the configured customFormats map through the obs, command, command-status, datastream-schema and command-stream-schema handlers so a registered format (e.g. application/swe+proto) can serve its own data and schema views. --- .../impl/service/consys/ConSysApiService.java | 8 +-- .../service/consys/obs/CustomObsFormat.java | 54 +++++++++++++++++-- .../consys/obs/DataStreamSchemaHandler.java | 23 +++++++- .../impl/service/consys/obs/ObsHandler.java | 10 ++-- .../service/consys/task/CommandHandler.java | 36 ++++++++++--- .../consys/task/CommandStatusHandler.java | 6 ++- .../task/CommandStreamSchemaHandler.java | 35 +++++++++--- 7 files changed, 145 insertions(+), 27 deletions(-) diff --git a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/ConSysApiService.java b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/ConSysApiService.java index 2671639ed..33ff879e9 100644 --- a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/ConSysApiService.java +++ b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/ConSysApiService.java @@ -262,7 +262,7 @@ protected void doStart() throws SensorHubException rootHandler.addSubResource(dataStreamHandler); systemsHandler.addSubResource(dataStreamHandler); sysMembersHandler.addSubResource(dataStreamHandler); - var dataSchemaHandler = new DataStreamSchemaHandler(handlerCtx, security.datastream_permissions); + var dataSchemaHandler = new DataStreamSchemaHandler(handlerCtx, security.datastream_permissions, customFormats); dataStreamHandler.addSubResource(dataSchemaHandler); // observations @@ -282,11 +282,11 @@ protected void doStart() throws SensorHubException rootHandler.addSubResource(cmdStreamHandler); systemsHandler.addSubResource(cmdStreamHandler); sysMembersHandler.addSubResource(cmdStreamHandler); - var cmdSchemaHandler = new CommandStreamSchemaHandler(handlerCtx, security.commandstream_permissions); + var cmdSchemaHandler = new CommandStreamSchemaHandler(handlerCtx, security.commandstream_permissions, customFormats); cmdStreamHandler.addSubResource(cmdSchemaHandler); - + // commands - var cmdHandler = new CommandHandler(handlerCtx, threadPool, security.command_permissions); + var cmdHandler = new CommandHandler(handlerCtx, threadPool, security.command_permissions, customFormats); cmdStreamHandler.addSubResource(cmdHandler); // command status diff --git a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/CustomObsFormat.java b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/CustomObsFormat.java index 8423a3b28..ae5fcd376 100644 --- a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/CustomObsFormat.java +++ b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/CustomObsFormat.java @@ -15,10 +15,13 @@ package org.sensorhub.impl.service.consys.obs; import java.io.IOException; +import org.sensorhub.api.command.ICommandData; +import org.sensorhub.api.command.ICommandStreamInfo; import org.sensorhub.api.common.BigId; import org.sensorhub.api.common.IdEncoders; import org.sensorhub.api.data.IDataStreamInfo; import org.sensorhub.api.data.IObsData; +import org.sensorhub.api.datastore.command.CommandStreamKey; import org.sensorhub.api.datastore.obs.DataStreamKey; import org.sensorhub.impl.service.consys.resource.RequestContext; import org.sensorhub.impl.service.consys.resource.ResourceBinding; @@ -26,11 +29,54 @@ public interface CustomObsFormat { + /** + * @return true if this format can encode observations of the given + * datastream. Drives the "formats" list advertised on the datastream + * resource and explicit format selection. + */ boolean isCompatible(IDataStreamInfo dsInfo); - - + + + /** + * @return true if this format should be auto-selected for browser requests + * with no explicit format (e.g. video formats playable in a browser). + * Defaults to {@link #isCompatible}; override to return false for formats + * that should be advertised but only served when explicitly requested. + */ + default boolean isAutoSelectable(IDataStreamInfo dsInfo) + { + return isCompatible(dsInfo); + } + + ResourceBinding getSchemaBinding(RequestContext ctx, IdEncoders idEncoders, IDataStreamInfo dsInfo) throws IOException; - - + + ResourceBinding getObsBinding(RequestContext ctx, IdEncoders idEncoders, IDataStreamInfo dsInfo) throws IOException; + + + /** + * @return a binding serving this format's view of a command stream's + * parameter schema, or null if this format does not support commands. + * Consulted by CommandStreamSchemaHandler when the commandFormat query + * param matches this format's mime type. + */ + default ResourceBinding getCommandSchemaBinding(RequestContext ctx, IdEncoders idEncoders, ICommandStreamInfo csInfo) throws IOException + { + return null; + } + + + /** + * @param forReading true when the binding will deserialize commands from + * the request body (POST/publish ingestion), false when it will + * serialize commands to the response (GET/stream) + * @return a binding encoding/decoding commands in this format, or null if + * this format does not support commands. Consulted by CommandHandler for + * both reads (GET/stream) and ingestion (POST with this content type). + */ + default ResourceBinding getCommandBinding(RequestContext ctx, IdEncoders idEncoders, ICommandStreamInfo csInfo, boolean forReading) throws IOException + { + return null; + } } diff --git a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/DataStreamSchemaHandler.java b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/DataStreamSchemaHandler.java index e648673a1..f243e8c3e 100644 --- a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/DataStreamSchemaHandler.java +++ b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/DataStreamSchemaHandler.java @@ -36,11 +36,20 @@ public class DataStreamSchemaHandler extends ResourceHandler { public static final String[] NAMES = { "schema" }; - - + + final Map customFormats; + + public DataStreamSchemaHandler(HandlerContext ctx, ResourcePermissions permissions) + { + this(ctx, permissions, java.util.Collections.emptyMap()); + } + + + public DataStreamSchemaHandler(HandlerContext ctx, ResourcePermissions permissions, Map customFormats) { super(ctx.getReadDb().getDataStreamStore(), ctx.getDataStreamIdEncoder(), ctx, permissions); + this.customFormats = customFormats != null ? customFormats : java.util.Collections.emptyMap(); } @@ -62,6 +71,16 @@ else if (format.equals(ResourceFormat.AUTO) && ctx.isBrowserHtmlRequest()) return new DataStreamBindingHtml(ctx, idEncoders, obsFormat); else if (obsFormat.isOneOf(ResourceFormat.JSON, ResourceFormat.OM_JSON)) return new DataStreamSchemaBindingOmJson(ctx, idEncoders, forReading); + // custom obs formats (e.g. application/swe+proto) provide their own schema + // binding — checked before the SWE_FORMAT_PREFIX branch since swe+proto + // also starts with "application/swe+" + else if (customFormats.containsKey(obsFormat.getMimeType())) + { + var dsInfo = dataStore.get(new DataStreamKey(ctx.getParentRef().internalID)); + if (dsInfo == null) + throw ServiceErrors.notFound(); + return customFormats.get(obsFormat.getMimeType()).getSchemaBinding(ctx, idEncoders, dsInfo); + } else if (obsFormat.getMimeType().startsWith(ResourceFormat.SWE_FORMAT_PREFIX)) return new DataStreamSchemaBindingSweCommon(obsFormat, ctx, idEncoders, forReading); else diff --git a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/ObsHandler.java b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/ObsHandler.java index a696407e1..5ccdd4ef3 100644 --- a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/ObsHandler.java +++ b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/ObsHandler.java @@ -169,7 +169,7 @@ protected ResourceBinding getCustomFormatBinding(RequestContext var mimeType = entry.getKey(); var formatImpl = entry.getValue(); - if (formatImpl.isCompatible(dsInfo)) + if (formatImpl.isAutoSelectable(dsInfo)) { obsFormat = formatImpl; ctx.getLogger().info("Auto-selecting format {}", mimeType); @@ -230,9 +230,13 @@ protected void subscribe(final RequestContext ctx) throws InvalidRequestExceptio var queryParams = ctx.getParameterMap(); var filter = getFilter(ctx.getParentRef(), queryParams, 0, Long.MAX_VALUE); - var responseFormat = parseFormat(queryParams); + // default to the format already set by the transport layer, if any + // (e.g. MQTT ":data/" subtopics); an explicit f= query + // param still takes precedence + var defaultFormat = ctx.getFormat() != null ? ctx.getFormat() : ResourceFormat.AUTO; + var responseFormat = parseFormat(queryParams, defaultFormat); ctx.setFormatOptions(responseFormat, parseSelectArg(queryParams)); - + // detect if real-time request boolean isRealTime = (filter.getPhenomenonTime() == null && filter.getResultTime() == null) || (filter.getResultTime() != null && filter.getResultTime().beginsNow()) || diff --git a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandHandler.java b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandHandler.java index 0af2e1241..e14c5153e 100644 --- a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandHandler.java +++ b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandHandler.java @@ -46,6 +46,7 @@ import org.sensorhub.impl.service.consys.HandlerContext; import org.sensorhub.impl.service.consys.ServiceErrors; import org.sensorhub.impl.service.consys.RestApiServlet.ResourcePermissions; +import org.sensorhub.impl.service.consys.obs.CustomObsFormat; import org.sensorhub.impl.service.consys.resource.BaseResourceHandler; import org.sensorhub.impl.service.consys.resource.IResourceHandler; import org.sensorhub.impl.service.consys.resource.RequestContext; @@ -68,9 +69,10 @@ public class CommandHandler extends BaseResourceHandler customFormats; final Random random = new SecureRandom(); - - + + public static class CommandHandlerContextData { public BigId streamID; @@ -78,16 +80,23 @@ public static class CommandHandlerContextData public BigId foiId; public CommandStreamTransactionHandler dsHandler; } - - + + public CommandHandler(HandlerContext ctx, ScheduledExecutorService threadPool, ResourcePermissions permissions) + { + this(ctx, threadPool, permissions, java.util.Collections.emptyMap()); + } + + + public CommandHandler(HandlerContext ctx, ScheduledExecutorService threadPool, ResourcePermissions permissions, Map customFormats) { super(ctx.getReadDb().getCommandStore(), ctx.getCommandIdEncoder(), ctx, permissions); - + this.eventBus = ctx.getEventBus(); this.db = ctx.getReadDb(); this.threadPool = threadPool; - + this.customFormats = customFormats != null ? customFormats : java.util.Collections.emptyMap(); + // I know the doc says otherwise but we need to use the federated DB for command transactions here // because we don't write to DB directly but rather send commands to systems that can be in other databases this.readOnlyTxHandler = new SystemDatabaseTransactionHandler(eventBus, ctx.getReadDb()); @@ -151,6 +160,15 @@ protected ResourceBinding getBinding(RequestContext ctx, bo } // select binding depending on format + // custom formats (e.g. application/swe+proto) take precedence when the + // requested mime type matches and the format supports commands + if (contextData.csInfo != null && customFormats.containsKey(format.getMimeType())) + { + var binding = customFormats.get(format.getMimeType()).getCommandBinding(ctx, idEncoders, contextData.csInfo, forReading); + if (binding != null) + return binding; + } + if (format.isOneOf(ResourceFormat.AUTO, ResourceFormat.JSON)) return new CommandBindingJson(ctx, idEncoders, forReading, dataStore); else @@ -181,7 +199,11 @@ protected void subscribe(final RequestContext ctx) throws InvalidRequestExceptio var queryParams = ctx.getParameterMap(); var filter = getFilter(ctx.getParentRef(), queryParams, 0, Long.MAX_VALUE); - var responseFormat = parseFormat(queryParams); + // default to the format already set by the transport layer, if any + // (e.g. MQTT ":data/" subtopics); an explicit f= query + // param still takes precedence + var defaultFormat = ctx.getFormat() != null ? ctx.getFormat() : ResourceFormat.AUTO; + var responseFormat = parseFormat(queryParams, defaultFormat); ctx.setFormatOptions(responseFormat, parseSelectArg(queryParams)); // continue when streaming actually starts diff --git a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStatusHandler.java b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStatusHandler.java index 19b0f780d..d55e81cf7 100644 --- a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStatusHandler.java +++ b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStatusHandler.java @@ -146,7 +146,11 @@ protected void subscribe(final RequestContext ctx) throws InvalidRequestExceptio var queryParams = ctx.getParameterMap(); var filter = getFilter(ctx.getParentRef(), queryParams, 0, Long.MAX_VALUE); - var responseFormat = parseFormat(queryParams); + // default to the format already set by the transport layer, if any + // (e.g. MQTT ":data/" subtopics); an explicit f= query + // param still takes precedence + var defaultFormat = ctx.getFormat() != null ? ctx.getFormat() : ResourceFormat.AUTO; + var responseFormat = parseFormat(queryParams, defaultFormat); ctx.setFormatOptions(responseFormat, parseSelectArg(queryParams)); // continue when streaming actually starts diff --git a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStreamSchemaHandler.java b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStreamSchemaHandler.java index 86c4f533d..13cbdb7d4 100644 --- a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStreamSchemaHandler.java +++ b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStreamSchemaHandler.java @@ -25,6 +25,7 @@ import org.sensorhub.impl.service.consys.HandlerContext; import org.sensorhub.impl.service.consys.ServiceErrors; import org.sensorhub.impl.service.consys.RestApiServlet.ResourcePermissions; +import org.sensorhub.impl.service.consys.obs.CustomObsFormat; import org.sensorhub.impl.service.consys.resource.RequestContext; import org.sensorhub.impl.service.consys.resource.ResourceBinding; import org.sensorhub.impl.service.consys.resource.ResourceFormat; @@ -36,30 +37,52 @@ public class CommandStreamSchemaHandler extends ResourceHandler { public static final String[] NAMES = { "schema" }; - - + + final Map customFormats; + + public CommandStreamSchemaHandler(HandlerContext ctx, ResourcePermissions permissions) + { + this(ctx, permissions, java.util.Collections.emptyMap()); + } + + + public CommandStreamSchemaHandler(HandlerContext ctx, ResourcePermissions permissions, Map customFormats) { super(ctx.getReadDb().getCommandStreamStore(), ctx.getCommandStreamIdEncoder(), ctx, permissions); + this.customFormats = customFormats != null ? customFormats : java.util.Collections.emptyMap(); } - - + + @Override protected ResourceBinding getBinding(RequestContext ctx, boolean forReading) throws IOException { var format = ctx.getFormat(); if (!format.isOneOf(ResourceFormat.AUTO, ResourceFormat.JSON)) throw ServiceErrors.unsupportedFormat(format); - + // generate proper schema depending on command format var cmdFormat = parseFormat("commandFormat", ctx.getParameterMap()); if (cmdFormat == null) cmdFormat = ResourceFormat.JSON; - + if (format.equals(ResourceFormat.AUTO) && ctx.isBrowserHtmlRequest()) return new CommandStreamBindingHtml(ctx, idEncoders); else if (cmdFormat.equals(ResourceFormat.JSON)) return new CommandStreamSchemaBindingJson(ctx, idEncoders, forReading); + // custom command formats (e.g. application/swe+proto) provide their own + // schema binding — checked before the SWE_FORMAT_PREFIX branch since + // swe+proto also starts with "application/swe+" + else if (customFormats.containsKey(cmdFormat.getMimeType())) + { + var csInfo = dataStore.get(new CommandStreamKey(ctx.getParentRef().internalID)); + if (csInfo == null) + throw ServiceErrors.notFound(); + var binding = customFormats.get(cmdFormat.getMimeType()).getCommandSchemaBinding(ctx, idEncoders, csInfo); + if (binding == null) + throw ServiceErrors.unsupportedFormat(cmdFormat); + return binding; + } else if (cmdFormat.getMimeType().startsWith(ResourceFormat.SWE_FORMAT_PREFIX)) return new CommandStreamSchemaBindingSweCommon(cmdFormat, ctx, idEncoders, forReading); else From 10fc506729c76f60eb81eaa1ba2f0bda232db9f5 Mon Sep 17 00:00:00 2001 From: Ian Patterson Date: Wed, 17 Jun 2026 14:56:34 -0500 Subject: [PATCH 2/4] feat(consys): route custom schema formats on datastream/controlstream POST Dispatch datastream and control-stream creation to a registered custom format's binding when the embedded schema names it. DataStreamBindingJson gains a custom-format branch ahead of the application/swe+ prefix match; CommandStreamBindingJson buffers the schema object and dispatches on commandFormat, staying backward-compatible (a missing commandFormat still uses the default JSON binding); CommandStreamHandler threads the customFormats map through to the binding. --- .../impl/service/consys/ConSysApiService.java | 2 +- .../consys/obs/DataStreamBindingJson.java | 9 +++ .../consys/task/CommandStreamBindingJson.java | 66 +++++++++++++------ .../consys/task/CommandStreamHandler.java | 17 +++-- 4 files changed, 70 insertions(+), 24 deletions(-) diff --git a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/ConSysApiService.java b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/ConSysApiService.java index 33ff879e9..93ff7cbaa 100644 --- a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/ConSysApiService.java +++ b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/ConSysApiService.java @@ -278,7 +278,7 @@ protected void doStart() throws SensorHubException //foiHandler.addSubResource(obsStatsHandler); // command streams - var cmdStreamHandler = new CommandStreamHandler(handlerCtx, security.commandstream_permissions); + var cmdStreamHandler = new CommandStreamHandler(handlerCtx, security.commandstream_permissions, customFormats); rootHandler.addSubResource(cmdStreamHandler); systemsHandler.addSubResource(cmdStreamHandler); sysMembersHandler.addSubResource(cmdStreamHandler); diff --git a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/DataStreamBindingJson.java b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/DataStreamBindingJson.java index 859110e11..5e3fb7318 100644 --- a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/DataStreamBindingJson.java +++ b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/obs/DataStreamBindingJson.java @@ -151,6 +151,15 @@ else if ("schema".equals(prop)) ResourceBindingJson schemaBinding = null; if (ResourceFormat.OM_JSON.getMimeType().equals(obsFormat)) schemaBinding = new DataStreamSchemaBindingOmJson(ctx, idEncoders, reader); + // custom obs formats (e.g. application/swe+proto) bring their own + // schema binding — checked BEFORE the SWE_FORMAT_PREFIX branch since + // swe+proto also matches that prefix (mirrors DataStreamSchemaHandler.getBinding) + else if (customFormats.containsKey(obsFormat)) + { + var custom = customFormats.get(obsFormat).getSchemaBinding(ctx, idEncoders, null); + if (custom instanceof ResourceBindingJson) + schemaBinding = (ResourceBindingJson) custom; + } else if (obsFormat.startsWith(ResourceFormat.SWE_FORMAT_PREFIX)) schemaBinding = new DataStreamSchemaBindingSweCommon(ResourceFormat.fromMimeType(obsFormat), ctx, idEncoders, reader); if (schemaBinding == null) diff --git a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStreamBindingJson.java b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStreamBindingJson.java index b57b19d18..c1f494476 100644 --- a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStreamBindingJson.java +++ b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStreamBindingJson.java @@ -15,8 +15,11 @@ package org.sensorhub.impl.service.consys.task; import java.io.IOException; +import java.io.StringReader; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.Map; import net.opengis.swe.v20.BinaryEncoding; import org.sensorhub.api.command.CommandStreamInfo; import org.sensorhub.api.command.ICommandStreamInfo; @@ -27,6 +30,8 @@ import org.sensorhub.impl.service.consys.InvalidRequestException; import org.sensorhub.impl.service.consys.ResourceParseException; import org.sensorhub.impl.service.consys.SWECommonUtils; +import org.sensorhub.impl.service.consys.ServiceErrors; +import org.sensorhub.impl.service.consys.obs.CustomObsFormat; import org.sensorhub.impl.service.consys.resource.RequestContext; import org.sensorhub.impl.service.consys.resource.ResourceBindingJson; import org.sensorhub.impl.service.consys.resource.ResourceFormat; @@ -37,6 +42,7 @@ import org.vast.swe.SWEStaxBindings; import org.vast.swe.json.SWEJsonStreamReader; import org.vast.swe.json.SWEJsonStreamWriter; +import com.google.gson.JsonParser; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; @@ -47,18 +53,26 @@ public class CommandStreamBindingJson extends ResourceBindingJson customFormats; SWEJsonStreamReader sweReader; SWEJsonStreamWriter sweWriter; - - + + public CommandStreamBindingJson(RequestContext ctx, IdEncoders idEncoders, IObsSystemDatabase db, boolean forReading) throws IOException + { + this(ctx, idEncoders, db, forReading, Collections.emptyMap()); + } + + + public CommandStreamBindingJson(RequestContext ctx, IdEncoders idEncoders, IObsSystemDatabase db, boolean forReading, Map customFormats) throws IOException { super(ctx, idEncoders, forReading); - + this.assocs = new CommandStreamAssocs(db, idEncoders); this.rootURL = ctx.getApiRootURL(); this.sweBindings = new SWEStaxBindings(); - + this.customFormats = customFormats != null ? customFormats : Collections.emptyMap(); + if (forReading) this.sweReader = new SWEJsonStreamReader(reader); else @@ -103,21 +117,35 @@ else if ("system@link".equals(prop)) sysRef = readFeatureRef(reader); else if ("schema".equals(prop)) { - reader.beginObject(); - - /*// obsFormat must come first! - if (!reader.nextName().equals("commandFormat")) - throw new ResourceParseException(MISSING_PROP_ERROR_MSG + "schema/obsFormat"); - var obsFormat = reader.nextString(); - - ResourceBindingJson schemaBinding = null; - if (ResourceFormat.JSON.getMimeType().equals(obsFormat)) - schemaBinding = new CommandStreamSchemaBindingJson(ctx, idEncoders, reader); - - if (schemaBinding == null) - throw ServiceErrors.unsupportedFormat(obsFormat);*/ - var schemaBinding = new CommandStreamSchemaBindingJson(ctx, idEncoders, reader); - csInfo = schemaBinding.deserialize(reader); + // Buffer the schema object so commandFormat can be inspected + // without destructively consuming the stream. This preserves + // backward compatibility with bodies that omit commandFormat or + // don't list it first (e.g. legacy "parametersSchema"-first), while + // still letting a custom format (application/swe+proto) opt in. + boolean wasLenient = reader.isLenient(); + reader.setLenient(true); + var schemaObj = JsonParser.parseReader(reader).getAsJsonObject(); + reader.setLenient(wasLenient); + + var commandFormat = schemaObj.has("commandFormat") + ? schemaObj.get("commandFormat").getAsString() : null; + var bufReader = new JsonReader(new StringReader(schemaObj.toString())); + + ResourceBindingJson schemaBinding; + if (commandFormat != null && customFormats.containsKey(commandFormat)) + { + var custom = customFormats.get(commandFormat).getCommandSchemaBinding(ctx, idEncoders, null); + if (!(custom instanceof ResourceBindingJson)) + throw ServiceErrors.unsupportedFormat(commandFormat); + schemaBinding = (ResourceBindingJson) custom; + } + else + { + // unchanged default: SWE-Common JSON via the JSON binding + schemaBinding = new CommandStreamSchemaBindingJson(ctx, idEncoders, bufReader); + } + + csInfo = schemaBinding.deserialize(bufReader); } else reader.skipValue(); diff --git a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStreamHandler.java b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStreamHandler.java index 5d7ed8bd2..888f2d410 100644 --- a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStreamHandler.java +++ b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/task/CommandStreamHandler.java @@ -30,6 +30,7 @@ import org.sensorhub.impl.service.consys.ConSysApiSecurity; import org.sensorhub.impl.service.consys.ServiceErrors; import org.sensorhub.impl.service.consys.RestApiServlet.ResourcePermissions; +import org.sensorhub.impl.service.consys.obs.CustomObsFormat; import org.sensorhub.impl.service.consys.resource.RequestContext; import org.sensorhub.impl.service.consys.resource.ResourceBinding; import org.sensorhub.impl.service.consys.resource.ResourceFormat; @@ -46,14 +47,22 @@ public class CommandStreamHandler extends ResourceHandler customFormats; + + public CommandStreamHandler(HandlerContext ctx, ResourcePermissions permissions) + { + this(ctx, permissions, java.util.Collections.emptyMap()); + } + + + public CommandStreamHandler(HandlerContext ctx, ResourcePermissions permissions, Map customFormats) { super(ctx.getReadDb().getCommandStreamStore(), ctx.getCommandStreamIdEncoder(), ctx, permissions); this.db = ctx.getReadDb(); this.transactionHandler = new SystemDatabaseTransactionHandler(ctx.getEventBus(), ctx.getWriteDb()); - + this.customFormats = customFormats != null ? customFormats : java.util.Collections.emptyMap(); + this.eventsHandler = new CommandStreamEventsHandler(ctx, permissions); addSubResource(eventsHandler); } @@ -70,7 +79,7 @@ protected ResourceBinding getBinding(Reque return new CommandStreamBindingHtml(ctx, idEncoders, db, true, title); } else if (format.isOneOf(ResourceFormat.AUTO, ResourceFormat.JSON)) - return new CommandStreamBindingJson(ctx, idEncoders, db, forReading); + return new CommandStreamBindingJson(ctx, idEncoders, db, forReading, customFormats); else throw ServiceErrors.unsupportedFormat(format); } From 706a2d31c3dd7f53b63f97071814a5eb6c9bb15e Mon Sep 17 00:00:00 2001 From: Ian Patterson Date: Mon, 6 Jul 2026 01:08:42 -0500 Subject: [PATCH 3/4] feat(consys): add RequestContext constructor for one-shot GET responses from alternate transports --- .../consys/resource/RequestContext.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/resource/RequestContext.java b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/resource/RequestContext.java index 318e7c180..b8a4c8304 100644 --- a/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/resource/RequestContext.java +++ b/sensorhub-service-consys/src/main/java/org/sensorhub/impl/service/consys/resource/RequestContext.java @@ -171,6 +171,25 @@ public RequestContext(RestApiServlet servlet, URI resourceURI, InputStream is) } + /* + * Constructor for other protocols providing their own path/params and output stream, + * for one-shot (non-streaming) GET responses (e.g. NATS request-reply read) + */ + public RequestContext(RestApiServlet servlet, URI resourceURI, OutputStream os) + { + this.clientSide = false; + this.servlet = Asserts.checkNotNull(servlet, Servlet.class); + this.req = null; + this.resp = null; + this.requestPathInfo = null; + this.streamHandler = null; + this.inputStream = null; + this.outputStream = Asserts.checkNotNull(os, OutputStream.class); + this.path = splitPath(resourceURI.getPath()); + this.queryParams = parseQueryParams(resourceURI.getQuery()); + } + + /* * Constructor for other protocols providing their own path/params and stream handler (e.g. MQTT subscribe) */ From 6988ded63679fc55b8741f92408a4d883e245d1e Mon Sep 17 00:00:00 2001 From: Ian Patterson Date: Tue, 28 Jul 2026 21:00:41 -0500 Subject: [PATCH 4/4] fix(webui): fix ClassCastException displaying range component values Range components (QuantityRange etc.) are SimpleComponents but not DataValues, so rendering their latest value in the admin UI crashed. getStringValue(SimpleComponent) handles scalars and ranges alike. --- .../src/main/java/org/sensorhub/ui/SWECommonForm.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sensorhub-webui-core/src/main/java/org/sensorhub/ui/SWECommonForm.java b/sensorhub-webui-core/src/main/java/org/sensorhub/ui/SWECommonForm.java index face5ac2b..0e2027e95 100644 --- a/sensorhub-webui-core/src/main/java/org/sensorhub/ui/SWECommonForm.java +++ b/sensorhub-webui-core/src/main/java/org/sensorhub/ui/SWECommonForm.java @@ -15,7 +15,6 @@ package org.sensorhub.ui; import org.sensorhub.ui.api.UIConstants; -import org.vast.data.DataValue; import org.vast.swe.SWEDataTypeUtils; import net.opengis.swe.v20.DataArray; import net.opengis.swe.v20.DataChoice; @@ -168,7 +167,11 @@ protected String getCaption(DataComponent dataComponent, boolean showValues) if (showValues && dataComponent.hasData()) { - var str = sweUtils.getStringValue((DataValue)dataComponent); + // cast to SimpleComponent (not DataValue): range components + // (QuantityRange, etc.) are SimpleComponents but not DataValues. + // getStringValue(SimpleComponent) already renders both scalars + // and ranges, so this is the same call for scalars. + var str = sweUtils.getStringValue((SimpleComponent)dataComponent); if ("NaN".equals(str) || "null".equals(str)) str = "n/a";