Class ConfigManager

java.lang.Object
com.ultikits.ultitools.manager.ConfigManager

public class ConfigManager extends Object
Version:
1.0.0
Author:
wisdomme
  • Field Details

  • Constructor Details

    • ConfigManager

      public ConfigManager()
  • Method Details

    • register

      public void register(UltiToolsPlugin ultiToolsPlugin, AbstractConfigEntity configEntity) throws IOException
      Register config entity.
      Parameters:
      ultiToolsPlugin - UltiTools module
      configEntity - Config entity
      Throws:
      IOException
    • addConfigEntity

      private void addConfigEntity(UltiToolsPlugin ultiToolsPlugin, AbstractConfigEntity configEntity)
    • snapshotRegisteredEntities

      private Map<String,AbstractConfigEntity> snapshotRegisteredEntities(UltiToolsPlugin ultiToolsPlugin)
      Snapshots the complete config-file-path-to-entity mapping ultiToolsPlugin already holds in pluginConfigMap, before a caller is about to register a batch of entities in one call. Used by register(UltiToolsPlugin, AbstractConfigEntity)'s directory branch, registerAll(UltiToolsPlugin, String, ClassLoader), and registerAll(UltiToolsPlugin, String[], ClassLoader) to restore precisely what existed before this call, on a refusal, without touching anything a prior call already committed (#358 Part 1).

      Gate-1 review finding: an earlier version of this snapshot captured only the KEY set, and rollBackRegisteredEntities(com.ultikits.ultitools.abstracts.UltiToolsPlugin, java.util.Map<java.lang.String, com.ultikits.ultitools.abstracts.AbstractConfigEntity>) retained those keys via retainAll rather than restoring their values. That is correct only when a batch never replaces an already- registered path with a different entity instance before failing - but a supported flow (e.g. registerAll scanning a path a module also registered directly via getAllConfigs()) can do exactly that, per ConfigManagerDualPathRegistrationTest. Capturing entity VALUES, not just keys, is what lets a rollback actually restore the original entity rather than silently keeping whatever replaced it.

      Parameters:
      ultiToolsPlugin - the plugin whose current registrations to snapshot
      Returns:
      a defensive copy of the currently-registered path-to-entity map, or an empty map if none
    • rollBackRegisteredEntities

      private void rollBackRegisteredEntities(UltiToolsPlugin ultiToolsPlugin, Map<String,AbstractConfigEntity> entitiesToRestore)
      Restores ultiToolsPlugin's entry in pluginConfigMap to exactly entitiesToRestore - not just the same key set, but the same path-to-entity VALUES, so a path this call replaced with a different entity before failing is restored to the entity that was actually there before the call started (see snapshotRegisteredEntities(com.ultikits.ultitools.abstracts.UltiToolsPlugin)'s javadoc for why key-only retention was insufficient). The cleanup half of #358 Part 1's all-or-nothing registration.
      Parameters:
      ultiToolsPlugin - the plugin to roll back
      entitiesToRestore - the path-to-entity map to restore, as captured before this call started
    • registerAll

      public void registerAll(UltiToolsPlugin plugin, String packageName, ClassLoader classLoader)
      Register all config entities in the specified package.
      Parameters:
      plugin - UltiTools module
      packageName - Package name
      classLoader - Class loader
    • registerAll

      public void registerAll(UltiToolsPlugin plugin, String[] packageNames, ClassLoader classLoader)
      Registers every @ConfigEntity class found across MULTIPLE scan packages for one plugin, as ONE atomic batch (CR-01, #358 Part 1 gate-1 finding).

      UltiToolsPlugin.initConfig() calls registerAll(UltiToolsPlugin, String, ClassLoader) once per surviving entry of DependencyUtils.getPluginPackages(plugin) - and #362's de-duplication only collapses NESTED scan packages, so two unrelated sibling packages (declared via @ComponentScan(basePackages = {...}) or @UltiToolsModule(scanBasePackages = {...})) both survive and are scanned in two SEPARATE calls. Each single-package call's own snapshot/rollback is correctly scoped to not disturb a PRIOR call's successful work - which means a refusal on the SECOND package left the FIRST package's already-committed entries stranded, because neither call's snapshot ever captured "before this plugin's whole scan", only "before this one call". This overload snapshots once, before any package in packageNames is scanned, and rolls back to that ONE snapshot if any package's scan refuses - so a plugin whose scan packages span more than one package registers all of them, or none.

      Parameters:
      plugin - UltiTools module
      packageNames - every package name to scan, in the order DependencyUtils.getPluginPackages returns them
      classLoader - Class loader
    • getConfigEntity

      public <T extends AbstractConfigEntity> T getConfigEntity(UltiToolsPlugin plugin, Class<T> type)
      Get config entity.
      Type Parameters:
      T - Config entity type
      Parameters:
      plugin - UltiTools module
      type - Config entity type
      Returns:
      Config entity
    • getConfigEntity

      public <T extends AbstractConfigEntity> T getConfigEntity(UltiToolsPlugin plugin, String path, Class<T> type)
      Get config entity by path.
      Type Parameters:
      T - Config entity type
      Parameters:
      plugin - UltiTools module
      path - Config entity path
      type - Config entity type
      Returns:
      Config entity
    • getConfigEntities

      public <T extends AbstractConfigEntity> List<T> getConfigEntities(UltiToolsPlugin plugin, Class<T> type)
      Get all config entities by type.
      Type Parameters:
      T - Config entity type
      Parameters:
      plugin - UltiTools module
      type - Config entity type
      Returns:
      Config entity list
    • getAllConfigEntities

      public Map<String,AbstractConfigEntity> getAllConfigEntities(UltiToolsPlugin plugin)
      Get all config entities for a plugin.
      Parameters:
      plugin - UltiTools module
      Returns:
      All config entities
    • reloadConfigs

      public void reloadConfigs(UltiToolsPlugin plugin)
      Reload all configs.
      Parameters:
      plugin - UltiTools module
    • saveAll

      public void saveAll()
      Saves, at shutdown, every registered configuration that module code changed in memory.

      Since 6.3.0 (#510) this writes only the entities whose AbstractConfigEntity.isModifiedSinceSnapshot() is true - whose current state differs from the state they last loaded or saved. A configuration no code changed is left alone, so an operator's edit to its file made while the server was running survives the restart. Before 6.3.0 every file was rewritten from memory and such edits were silently discarded.

      A configuration whose file failed to parse the last time it was read is never written: the framework does not know what that file holds, so overwriting it would destroy an operator's broken file. One WARNING per such file says so, and any in-memory change to it is not written.

      If an entity was changed in memory and its file was also changed on disk since that snapshot, the in-memory state still wins and is written, and a WARNING names the file whose edits were overwritten. The only caller is UltiTools#onDisable(); an explicit AbstractConfigEntity.save() is unaffected and always writes.

      Each entity's check-then-save runs under that entity's own monitor, the lock its read, write and snapshot paths also hold, so a panel write still in flight on the WebSocket thread is applied either wholly before or wholly after this entity's shutdown save. A save failure, or any unchecked exception from one entity, is logged and does not stop the remaining entities from being saved.

    • warnUnparseableFileLeftAlone

      private void warnUnparseableFileLeftAlone(AbstractConfigEntity config)
      Logs that the shutdown save left a configuration file alone because the framework could not parse it the last time it read it (#510), and that any in-memory change to that configuration was therefore not written.
      Parameters:
      config - the entity that was skipped
    • warnOperatorEditOverwritten

      private void warnOperatorEditOverwritten(AbstractConfigEntity config)
      Logs that the shutdown save wrote an in-memory change over a file that was changed or removed on disk while the server was running (#510). The overwrite itself is the documented contract - a value set from code is saved on disable - but it must not be silent.
      Parameters:
      config - the entity that was just saved
    • buildJsonFromConfigs

      private String buildJsonFromConfigs(Function<AbstractConfigEntity,com.google.gson.JsonObject> extractor)
      Builds a JSON string from all config entities using the provided extractor function.
      Parameters:
      extractor - function to extract JsonObject from config entity
      Returns:
      JSON string
    • getComments

      public final String getComments()
      Get all comments.
      Returns:
      all comments
    • toJson

      public final String toJson()
      Cast config to JSON format.
      Returns:
      config in JSON format
    • loadFromJson

      public final void loadFromJson(String json) throws IOException
      Load config from JSON string.

      Since 6.3.0, a value violating its @Range/@NotEmpty/@Size/ @Pattern constraint refuses with ConfigurationException instead of being written - the operator's file is not modified for that config entity (SILENT-14).

      Since gate-1 CR-02 (#358 Part 2), the WHOLE batch this call touches - potentially several config entities across several plugins in one JSON payload - is VALIDATED before any of them is persisted: a first pass calls AbstractConfigEntity.validateProposedProperties(com.google.gson.JsonObject) on every touched entity (applying nothing to disk, restoring every field it touched regardless of outcome), and only once every entity in the batch has passed does a second pass call AbstractConfigEntity.updateProperties(com.google.gson.JsonObject) on each to actually apply and persist. A validation refusal on entity N therefore leaves entities 1..N-1 exactly as they were before this call - none of them written to disk - rather than the pre-CR-02 behaviour where files 1..N-1 were already applied and persisted by the time entity N's refusal was discovered.

      This guarantee covers VALIDATION refusals only, not a physical I/O failure during the second pass's own persist step (gate-1 review, line 425): if entity K's own config.save(File) throws IOException - a disk-full or permissions failure, not a validation constraint - entities 1..K-1 have already been applied and persisted by that point, and this call still throws, leaving a partially-applied batch. Making the persist phase itself durable against a physical write failure across N independent files would need staged writes (temp file + atomic rename) or a byte-level undo log for every touched file, which is a materially larger change than this fix's scope (see the follow-up issue filed for it). This is the same shape as #469 (WR-01): the registry-level guarantee this class makes is not a filesystem-durability guarantee.

      Parameters:
      json - JSON string
      Throws:
      IOException - if an I/O error occurs while persisting - entities already persisted earlier in this batch are NOT rolled back
      ConfigurationException - if a value violates its validation constraint - nothing in this call's batch is persisted when this is thrown, since validation runs to completion across the whole batch before persistence starts
    • loadFromJson

      public final void loadFromJson(String configFilePath, String json) throws IOException
      Load a single config file from a JSON string.

      loadFromJson(String) accepts the full nested structure {pluginName: {configPath: {key: value}}} -- the same shape toJson() produces. This method accepts just the innermost layer -- one config file's own {key: value} map -- and writes it to whichever file configFilePath names. The panel uses this narrower shape when pushing a single config by filename; see issue #236.

      A config path is unique within a single plugin (it is the inner key of pluginConfigMap), but may collide across plugins. Both "not found" and "matched more than one" throw rather than silently doing nothing -- "the caller thinks it wrote something and nothing actually happened" was exactly the original defect on this path.

      Since 6.3.0, a value violating its @Range/@NotEmpty/@Size/ @Pattern constraint refuses with ConfigurationException instead of being written - the operator's file is not modified (SILENT-14).

      Parameters:
      configFilePath - config file path as registered, e.g. config/lang.yml
      json - JSON object of that file's entries
      Throws:
      IOException - if the path is blank, unknown, ambiguous, or the JSON is not an object, or if saving fails
      ConfigurationException - if a value violates its validation constraint
      Since:
      6.2.5