Class RemoteConfigHandler

java.lang.Object
com.ultikits.ultitools.utils.RemoteConfigHandler

final class RemoteConfigHandler extends Object
The three structured-configuration requests of protocol v2: browse the registered configuration files, read one file's typed entries with the version of its bytes, and write exactly the keys the panel names - only while the file still holds the bytes the panel read.

Why this cannot overwrite operator content. Nothing here edits a file by rendering a new whole document. A write names the paths it owns (OwnedPaths) and goes through OperatorFileWriter, which re-reads the file at write time, applies only those settings, renders the document from its own node tree and then refuses the write unless every line outside the owned settings - comments, order, quoting, other values - comes out byte-identical. The panel's baseVersion is the SHA-256 of the raw bytes it read; the same value is handed to the gate as the expected fingerprint, so an operator's edit made after the read answers file_changed and nothing is written. A file the operator marked read-only is refused before anything is staged.

The only write path here is an explicit panel edit of the named keys, guarded by that version; nothing in this class writes at start-up, at shutdown, on a reload or on a timer. The values are checked per key before the write - the entry's declared type and its @Range bounds here, and the entity's own validators (@Range, @NotEmpty, @Size, @Pattern and its declared-type converters) through AbstractConfigEntity.validateProposedProperties(com.google.gson.JsonObject) - so a refused key never reaches the file and the accepted ones are written in one verified replace.

Every edit is recorded. As the 6.3.0 configuration edit did, each config.update that reached a decision writes one action-log entry (capability FILE_WRITE, action config.update, the request's path as target, panel as actor): ALLOWED when at least one key was written, DENIED with the text the panel received when nothing was. Values are never recorded. config.tree and config.entries record nothing; a request the switch gate refused is recorded by the gate.

All file work runs on the thread the caller injects, which must be the server thread: the configuration layer refuses registry access anywhere else (ConfigManager.permitsConfigThread(com.ultikits.ultitools.abstracts.UltiToolsPlugin, java.lang.String)). Payload validation runs on the dispatching thread so a malformed request still becomes the router's correlated invalid_message.

Known limitation (niche case, documented rather than defended): a writer outside the framework that replaces the file between the gate's last read and its atomic rename is not detected; the gate's own re-read immediately before publishing is what narrows that window.

Since:
6.3.1
  • Field Details

  • Constructor Details

    • RemoteConfigHandler

      RemoteConfigHandler(Supplier<List<UltiToolsPlugin>> modules, ConfigManager configs, Executor configThread)
      Creates the handler that publishes paths relative to the process working directory, the root the remote file surface already uses.
      Parameters:
      modules - the loaded modules whose registered configuration files may be shown
      configs - the configuration registry, for listing registered files and reloading a module
      configThread - the executor that must run the server thread
    • RemoteConfigHandler

      RemoteConfigHandler(Supplier<List<UltiToolsPlugin>> modules, ConfigManager configs, Executor configThread, File serverRoot)
      Creates the handler with an explicit server root; tests only, since the panel's paths are the paths a server operator would see under the working directory.
      Parameters:
      modules - the loaded modules whose registered configuration files may be shown
      configs - the configuration registry
      configThread - the executor that must run the server thread
      serverRoot - the directory every published path is relative to
    • RemoteConfigHandler

      RemoteConfigHandler(Supplier<List<UltiToolsPlugin>> modules, ConfigManager configs, Executor configThread, File serverRoot, Consumer<RemoteActionLog.Entry> actionLog)
      Creates the handler with an explicit server root and the action log that receives one entry per config.update request.
      Parameters:
      modules - the loaded modules whose registered configuration files may be shown
      configs - the configuration registry
      configThread - the executor that must run the server thread
      serverRoot - the directory every published path is relative to
      actionLog - receives one entry per config.update request that reached a decision, or null when the caller has no action log to write to (tests)
  • Method Details

    • register

      void register(AgentMessageRouter router)
      Registers the three request types on the protocol router.
      Parameters:
      router - the router that dispatches inbound frames
    • handleTree

      private void handleTree(CloudRequest request, CloudChannel channel)
      Answers the configuration file tree: each loaded module with the files it registered.
    • handleEntries

      private void handleEntries(CloudRequest request, CloudChannel channel)
      Answers one file's typed entries and the SHA-256 of its current bytes.
    • handleUpdate

      private void handleUpdate(CloudRequest request, CloudChannel channel)
      Writes exactly the keys a panel edit names, and only while the file still holds the bytes it read.
    • tree

      private com.google.gson.JsonElement tree()
    • modulesByName

      private Map<String,UltiToolsPlugin> modulesByName()
    • entries

      private void entries(CloudChannel channel, String replyTo, String path)
    • update

      private void update(CloudChannel channel, String replyTo, String path, String baseVersion, List<RemoteConfigHandler.Change> changes, RemoteConfigHandler.UpdateAudit audit)
    • write

      private void write(CloudChannel channel, String replyTo, RemoteConfigHandler.Registered registered, ConfigLoadResult loaded, List<RemoteConfigHandler.Change> changes, RemoteConfigHandler.UpdateAudit audit)
    • writeDenied

      private static String writeDenied(String reason)
      The panel's explanation of a write the gate refused: the gate's own reason, which names the line an operator has to fix and never a value. The fixed sentence it replaces read like a permission problem and left the actionable part in the server log only.
      Parameters:
      reason - the gate's value-free reason, empty when it wrote or found nothing to write
      Returns:
      the message for the write_denied error
    • refuseUpdate

      private static void refuseUpdate(CloudChannel channel, String replyTo, RemoteConfigHandler.UpdateAudit audit, String code, String message)
      Answers a refused config.update and records the same text as its one DENIED action-log entry, so the log says what the operator was told.
    • nothingWritten

      private static String nothingWritten(List<RemoteConfigHandler.KeyResult> results)
      The action-log reason of an update whose every key was refused: the key names with the per-key messages the panel received. Both are value-free: a key is a setting's name, and every per-key message names a rule.
      Parameters:
      results - the per-key results, none of them ok
      Returns:
      the bounded reason
    • entityRefusal

      private String entityRefusal(AbstractConfigEntity entity, RemoteConfigHandler.Change change, RemoteConfigEntries.Entry entry)
      The entity's own check of one change: it applies the value to the declared field in memory, runs the same validators and converters a load runs, and restores every field before returning.

      The text handed to the panel names the rule, never the value and never the load-time wording: the configuration layer's own message echoes the offending value and speaks of a module that refused to load, which is not what happened to a panel edit. Only the error code - the class of rule that refused - is forwarded.

    • refusalText

      private static String refusalText(ConfigurationException refusal)
    • owned

      private static OwnedPaths owned(List<RemoteConfigHandler.Accepted> accepted)
    • reload

      private String reload(RemoteConfigHandler.Registered registered)
      Makes the module take the values just written, by reloading the one entity that owns the written file. Only that entity is reloaded: reloading every file of the module would let a broken file this write never touched block the reload of the edited one (ConfigManager.reloadConfigs(com.ultikits.ultitools.abstracts.UltiToolsPlugin) stops at the first failure), and the panel would be told the edit failed when the file it edited was written and readable.
      Parameters:
      registered - the file that was written and its owning entity
      Returns:
      the panel's explanation when the module did not take the new values, or null when it did
    • keysData

      private static com.google.gson.JsonObject keysData(List<RemoteConfigHandler.KeyResult> results, String version)
    • lookup

      private RemoteConfigHandler.Registered lookup(String path)
    • publishedPath

      private String publishedPath(UltiToolsPlugin module, String configFilePath)
      The panel's path of one registered configuration file: the file's location relative to the server root, with / separators, as the contract's path primitive requires. A file outside the root, or a name the primitive rejects, is not published at all - the panel never sees a path it could not name back.
    • lockFor

      private static Object lockFor(Path file)
    • onConfigThread

      private void onConfigThread(CloudChannel channel, String replyTo, String type, Runnable work)
    • failed

      private void failed(CloudChannel channel, String replyTo, String type, RuntimeException failure)
    • bounded

      private static String bounded(String message)
    • changes

      private static List<RemoteConfigHandler.Change> changes(com.google.gson.JsonArray items, String id)
      Validates every change item against the frozen config.update schema shapes and copies it. A malformed item is an InvalidFrameException, so the router answers invalid_message.
    • value

      private static com.google.gson.JsonElement value(com.google.gson.JsonElement value, String id)
    • invalid

      private static InvalidFrameException invalid(String id)