Class AbstractConfigEntity

java.lang.Object
com.ultikits.ultitools.abstracts.AbstractConfigEntity

public abstract class AbstractConfigEntity extends Object
A module configuration bound through the converter registry and a comment-preserving document. All framework operations run under the entity monitor; module field setters remain unsynchronized. Constructors must be cheap and side-effect-free: validation constructs one throwaway instance to prove the existing String/no-arg construction contract. Snapshot checks never construct entities. The persisted raw document and the ordered effective field baseline are separate: partial panel writes must not acknowledge unrelated unsaved code edits. Missing reload keys retain live fields, but their baseline remains the initial declared default.
  • Field Details

    • ABSENT_RELOAD_VALUE

      private static final Object ABSENT_RELOAD_VALUE
    • LOGGER

      private static final Logger LOGGER
    • NUMBER_KINDS

      private static final List<Class<?>> NUMBER_KINDS
      Representative classes of each kind a constraint checks, for "can a value of the declared type be one of them".
    • TEXT_KINDS

      private static final List<Class<?>> TEXT_KINDS
    • CONTAINER_KINDS

      private static final List<Class<?>> CONTAINER_KINDS
    • configFilePath

      private final String configFilePath
    • changeListeners

      private final List<ConfigChangeListener> changeListeners
    • ultiToolsPlugin

      private UltiToolsPlugin ultiToolsPlugin
    • document

      private ConfigDocument document
    • lastLoadedPresence

      private Map<String,Object> lastLoadedPresence
    • acknowledgedRaw

      private final Map<Field,AbstractConfigEntity.RawEntry> acknowledgedRaw
    • declaredDefaults

      private final Map<Field,Object> declaredDefaults
    • declaredDefaultLengths

      private final Map<Field,Integer> declaredDefaultLengths
      The declared default's own size per field, measured on the Java value when the defaults are captured (0 for an empty or null container, -1 for a value @Size does not count): a converter or a legacy parser may give the default another plain shape, such as a set written as one joined: entry (PR #632 local Codex run 1).
    • savedSnapshot

      private Map<Field,Object> savedSnapshot
    • savedFileFingerprint

      private String savedFileFingerprint
    • lastLoadUnparseable

      private boolean lastLoadUnparseable
    • lastInitIncomplete

      private volatile boolean lastInitIncomplete
    • defaultsCaptured

      private boolean defaultsCaptured
    • validatingReload

      private boolean validatingReload
      Set only while a reload validates the values it read, for the refusal's wording (#595).
    • deferInitialization

      private boolean deferInitialization
    • pendingInitialization

      private AbstractConfigEntity.PendingInitialization pendingInitialization
    • commentWriteNotAppliedAtLoad

      private boolean commentWriteNotAppliedAtLoad
      Whether the last load's comment-only write was refused or failed (it has logged its one warning).
    • fileNotRestored

      private boolean fileNotRestored
      Whether a write to this file failed part-way and the file could not be put back from its framework backup (#622): it may hold part of that write, so the entity treats it as changed since it was read and writes nothing to it until a load reads it again. Deliberately not part of AbstractConfigEntity.PanelCheckpoint: rolling an entity's state back never forgets that its file is in an unknown state.
    • warnedCommentKeys

      private final Set<String> warnedCommentKeys
    • compositeReads

      private final Map<Field,Map<List<String>,AbstractConfigEntity.RawEntry>> compositeReads
      The composite values a panel edit is about to rewrite whole, as last read, per setting and unit; set by applyAndValidate and consumed by panelChanges under the entity monitor (route change, R3-01).
    • compositeWrites

      private final Map<Field,Map<List<String>,Object>> compositeWrites
      The whole composite values a panel edit writes, per setting and unit: the value as last read with only the edited fields changed, so every untouched field keeps the bytes the operator wrote (#609, 17-65 round 4 R4-I2); set by applyAndValidate and consumed by panelChanges under the entity monitor, like compositeReads.
    • bindingRanges

      private final Map<String,Map<String,Predicate<Long>>> bindingRanges
      The value ranges a config binding (#531) imposes on this entity's keys, by @ConfigEntry path, then by rule text. A panel write (updateProperties(com.google.gson.JsonObject), validateProposedProperties(com.google.gson.JsonObject)) is refused when it sets a bound key outside its range, the same way as a @Range violation. init(com.ultikits.ultitools.abstracts.UltiToolsPlugin) and reload() do not consult this: the binding step handles an invalid value there (it refuses the module at load and keeps the running value on reload).
    • NOT_RESTORED

      private static final String NOT_RESTORED
      The refusal reason while fileNotRestored holds (#622): a phrase without any value.
      See Also:
  • Constructor Details

    • AbstractConfigEntity

      public AbstractConfigEntity(String configFilePath)
      Constructor for AbstractConfigEntity.
      Parameters:
      configFilePath - the path to the configuration file, for example: config/config.yml
  • Method Details

    • save

      public void save() throws IOException
      Writes the settings this module changed in memory since the last load or save - and nothing else - through the config write gate.

      A setting is written only when all three hold: the module changed it since the last load or save; the file still holds at that path exactly the value the framework last read or wrote there; and that value is the one the module started from (it converts to the setting's value as last loaded or saved). A setting declared as a Map is compared and written entry by entry, following the declared map types into nested maps: only the entries the module added, changed or removed are set or removed. Any other value - a list, a ConfigurationSerializable such as a Bukkit Location, the value of a typed map entry that is not itself a map - is one value, written whole or not at all, so the file never holds a value mixed from the module's and the operator's. Everything else in the file stays as it is: a save never inserts a key the file lacks, never rewrites a comment and never removes a map entry the module did not remove. When nothing needs writing, the file is not touched (bytes and modification time stay).

      Why it cannot overwrite operator content. A value the operator edited on disk since it was read, a key the operator deleted, a value the framework could not use (interval: 3O0, a list element abc) and a map entry left in the 6.2 split form (o: {O: x}) are never the value the module started from at that path, so they are never written - whether or not the module changed that setting. The config write gate (OperatorFileWriter) then verifies, after rendering, that every line outside the written settings is byte-identical to the file as it is at write time, or writes nothing. A change that is not written stays in memory, and one WARNING names the file and those keys, never a value (maintainer decision 2026-10-04, "what code may write, by file type", which supersedes the earlier "a save warns and overwrites").

      Use it for a change the operator asked for through the module, or for shipped text the module re-renders after a language switch. A failed write leaves the effective baseline as it was, so the change is still unsaved.

      Throws:
      IOException - if publishing the verified file fails; the file then holds the bytes it held before, put back from its framework backup when an in-place write failed part-way, or - when even that fails - one SEVERE names the file and the kept backup and later saves write nothing until a reload
    • apply

      private static void apply(ConfigDocument target, List<AbstractConfigEntity.ModuleChange> changes)
      Sets the module's value, or removes the map entry the module removed, at each change's path.
    • saveOwnership

      private static OwnedPaths saveOwnership(List<AbstractConfigEntity.ModuleChange> changes, ConfigDocument read)
      What a save owns at the config write gate: each change's whole key, except that a map which is empty in the file as read ({}), or which the changes empty, is owned as a whole - its key line must change with its first or last entry, and an empty map holds nothing of the operator's that could be lost.
    • saveModuleChanges

      private void saveModuleChanges() throws IOException
      The save rule of save(): collects the module's changes against the effective baseline; keeps those whose last-read file value is the value the module started from; checks each on one read of the file, which must still hold there what the framework last read (a file already holding the module's value needs nothing); writes the rest through the config write gate against exactly that read; advances the baseline and the last-read entry only for what the file now holds; and names every change that was not written in one warning.
      Throws:
      IOException
    • acknowledgeWritten

      private void acknowledgeWritten(String readFingerprint, String fingerprint, ConfigDocument onDisk, List<AbstractConfigEntity.ModuleChange> applied)
      Advances the effective baseline and the last-read entry of exactly the changes the file now holds. The entity records the bytes the gate wrote as last read only when the gate's read held exactly the bytes this entity last bound; otherwise the operator's newer edit stays a change on disk (never a fresh read: 17-63 review WR-01).
      Parameters:
      readFingerprint - the bytes the write was checked against (storage-layer SHA-256 hex)
      fingerprint - the bytes the file holds now (the gate's written bytes, or the bytes read when nothing was written)
      onDisk - the document the file holds now
      applied - the changes the file now holds
    • startedFromFile

      private boolean startedFromFile(Field field, List<String> leaf, Object base)
      Whether the value the framework last read for field at leaf is the value the module started from there: it is present, and the setting's last loaded or saved value with that entry taken from the file converts, without a conversion failure, back to exactly that value. An unconvertible value or element, a key the operator deleted and a 6.2 split map entry are therefore never "started from".
    • changedLeaves

      private static void changedLeaves(Object base, Object current, Type declared, List<String> prefix, List<List<String>> out)
      Collects the paths, relative to a setting, at which current differs from base. Only a value declared as a Map is compared key by key (an added or removed key is one path), following the declared value type into nested maps; anything else - a list, a ConfigurationSerializable such as a Bukkit Location or Vector, a typed map's value of a non-map type - is one value, written whole or not at all, so a save never leaves on disk a value mixed from the module's and the operator's (save rule revision 1, 17-65 review round 1 R65-01).
    • mapValueType

      private static Type mapValueType(Type declared)
      The declared value type of a map type (Object when it is raw).
    • splitDepth

      private static int splitDepth(Type declared, List<String> keys)
      How many of keys, from the start, address entries of declared maps: the keys after them would reach inside a value that is one value (17-65 review round 2 R2-01).
    • splitsByEntry

      private static boolean splitsByEntry(Type declared, Object base, Object current)
      Whether a value is compared entry by entry: it is declared as a Map and both plain forms are maps that are not a serialized object (Bukkit's == type key).
    • describeKey

      private String describeKey(Field field, List<String> leaf)
      The key a warning names for a change: the setting's path, then the map keys, any key below a secret-shaped one redacted.
    • displayKeys

      private List<String> displayKeys(boolean secretAbove, List<String> keys)
      The one display rule for map keys in every configuration diagnostic bound for the server log or the panel (#673, case 54): once the setting or a key above is secret-shaped, every deeper key shows as <redacted>; the secret-shaped key itself is shown, so the operator still sees which setting to fix. Only the displayed text is redacted - whether a value is secret is decided from the real keys (secretPath(java.lang.reflect.Field, java.util.List<java.lang.String>)), never from this text.
      Parameters:
      secretAbove - whether a key above keys - the field name or a declared path segment - is secret-shaped
      keys - the real keys, outermost first
      Returns:
      the keys to display, in the same order
    • secretSetting

      private boolean secretSetting(Field field)
      Whether a setting is secret-shaped: its field name or a segment of its declared path.
    • secretPath

      private boolean secretPath(Field field, List<String> leaf)
      Whether a value of field at leaf is secret: the setting or a real key down to it is secret-shaped.
    • saveOperatorChange

      public final void saveOperatorChange(String... entryPaths) throws IOException
      Writes exactly the named settings, as the module holds them now, because the operator explicitly asked for that change - a command such as /setspawn, which names the six spawn.location.* entries.

      The operator's request is their consent to replace what the file holds at those settings: the module's value is written there even when the operator also edited that key by hand since it was read (the command wins at the key it names), and a named setting the file lacks is inserted with its comment. Nothing else is written - not another setting, not even one the module changed and did not name - and no other key, value, comment or byte of the file moves (maintainer decision of 2026-10-04, "what code may write, by file type": write exactly the item the operator explicitly asked to change).

      Naming a setting declared as a Map writes the whole map as the module holds it: every entry the operator added or edited by hand in that map since it was read is replaced or dropped. For a command that changes one entry - one rule, one warp - use saveOperatorMapEntry(String, String...), which writes only that entry.

      Why it cannot overwrite other operator content. The write goes through the framework's configuration write gate owning only the named settings' keys: after rendering, every line outside them must be byte-identical to the file as it is at write time, or nothing is written. When the gate refuses - the file cannot be read or parsed, uses anchors, aliases or merge keys, has a layout the write could not keep byte for byte outside the named keys, or changed while the write was being prepared - this throws ConfigWriteRefusedException naming the reason; the file keeps its bytes and the in-memory values stay as the module set them, still unsaved.

      Parameters:
      entryPaths - the ConfigEntry.path() of each setting to write (the field name for an entry declared without a path), at least one
      Throws:
      IllegalArgumentException - if no path is given, or a path is not a declared entry of this configuration; nothing is written
      IllegalStateException - if called before init, or off the server thread while a server runs
      ConfigWriteRefusedException - if the configuration write gate refused the write; nothing is written
      IOException - if publishing the verified file fails; the file then holds the bytes it held before, put back from its framework backup when an in-place write failed part-way, or - when even that fails - one SEVERE names the file and the kept backup, and this configuration treats the file as changed since it was read: every later write is refused until a reload reads it again
      Since:
      6.3.0
    • saveOperatorMapEntry

      public final void saveOperatorMapEntry(String entryPath, String... mapKeys) throws IOException
      Writes exactly one entry of a map setting, as the module holds it now, because the operator explicitly asked for that change - a command such as /autoreply add, which names one rule of autoreply.rules.

      The entry is set to the module's value, inserted when the file lacks it; when the module's map no longer holds the entry, the entry is removed from the file. The operator's request is their consent to replace what the file holds at that entry. Every other entry - one the operator added or edited by hand since the file was read, a 6.2 split entry - and every other key, value, comment and byte of the file stay (maintainer decision of 2026-10-04: write exactly the item the operator explicitly asked to change). Each map key is one whole key: a rule named play.example is the single key play.example.

      Why it cannot overwrite other operator content. As saveOperatorChange(String...): the write owns only that entry's key at the configuration write gate, which refuses any change to another byte of the file and then throws ConfigWriteRefusedException naming the reason, leaving the file and the in-memory value as they are. A map that is empty in the file ({}), or that the change empties, is owned as a whole, since its key line changes with its first or last entry.

      Parameters:
      entryPath - the ConfigEntry.path() of a setting declared as a Map
      mapKeys - the keys from that map down to the entry, one whole key each; usually just the entry's key
      Throws:
      IllegalArgumentException - if entryPath is not a declared entry of this configuration, the setting is not declared as a map, no key is given, or the keys reach inside an entry that is not itself a declared map (a serializable such as a Location, or a list, is one value: name the entry itself); nothing is written
      IllegalStateException - if called before init, or off the server thread while a server runs
      ConfigWriteRefusedException - if the configuration write gate refused the write; nothing is written
      IOException - if publishing the verified file fails; the file then holds the bytes it held before, put back from its framework backup when an in-place write failed part-way, or - when even that fails - one SEVERE names the file and the kept backup, and this configuration treats the file as changed since it was read: every later write is refused until a reload reads it again
      Since:
      6.3.0
    • saveOperatorMapEntry

      public final void saveOperatorMapEntry(EntryPresence required, String entryPath, String... mapKeys) throws IOException
      Writes exactly one entry of a map setting, as saveOperatorMapEntry(String, String...) does, but only when the entry's presence in the file is what the operator's command assumes: EntryPresence.MUST_BE_ABSENT for a command that creates the entry (/autoreply add <name>), EntryPresence.MUST_BE_PRESENT for one that changes an existing entry (/autoreply setkeyword <name>). When the condition does not hold, nothing is written and ConfigEntryPresenceException tells the caller which condition failed. A module then needs no second read of the file to decide whether the command may write.

      What "present" means. The entry is present when its whole key path exists in the file. The path is where the framework reads the setting (its nested keys, or the flat dotted key the operator wrote), followed by mapKeys, each one whole key. An entry holding null is present. An entry is absent when its key, or a key above it, is missing, null, an empty map or not a map. A file that is missing, empty or holds only comments holds no entry (see EntryPresence).

      Why it cannot overwrite other operator content. As saveOperatorMapEntry(String, String...): the write owns only that entry's key at the configuration write gate, which refuses any change to another byte of the file. The condition adds no second read. It is decided on the same read of the file that the gate verifies the edit against and that the gate checks again immediately before it replaces the file. An entry the operator added or deleted by hand before that read is seen. An edit saved after it makes the gate refuse the write, because the file changed. So add never replaces an entry the operator wrote, and setkeyword never writes back an entry the operator deleted.

      A failed condition logs at FINE only, because the caller reports it to the operator. Every other refusal is as for the two-argument overload: one WARNING names the file and the keys, never a value.

      Parameters:
      required - whether the entry must already be in the file, or must not be
      entryPath - the ConfigEntry.path() of a setting declared as a Map
      mapKeys - the keys from that map down to the entry, one whole key each; usually just the entry's key
      Throws:
      IllegalArgumentException - if required is null, or for any reason the two-argument overload names; nothing is written
      IllegalStateException - if called before init, or off the server thread while a server runs
      ConfigEntryPresenceException - if the entry's presence in the file is not required; nothing is written
      ConfigWriteRefusedException - if the configuration write gate refused the write; nothing is written
      IOException - if publishing the verified file fails; the file then holds the bytes it held before, put back from its framework backup when an in-place write failed part-way, or - when even that fails - one SEVERE names the file and the kept backup, and this configuration treats the file as changed since it was read: every later write is refused until a reload reads it again
      Since:
      6.3.0
    • writeOperatorMapEntry

      private void writeOperatorMapEntry(EntryPresence required, String entryPath, String... mapKeys) throws IOException
      Both saveOperatorMapEntry overloads: required is null for the unconditional one.
      Throws:
      IOException
    • spelledLeaf

      private List<String> spelledLeaf(Field field, List<String> leaf, Object mine) throws ConfigWriteRefusedException
      The keys of an operator's map-entry write, spelled as the module's map and the file spell them (#673, case 52). At a map level declared with a non-text key type (Integer, Long, Boolean, ...), a key the module's plain map there does not hold as given is read through that key type and replaced by the one key of the module's map that reads back to the same key, else by the one such key of the value last read from the file, so "1" reaches the entry the file spells '01'. Text that does not convert, or that no key reads back to, is kept as given; levels keyed by String or Object are not changed. A setting with its own legacy ConfigEntry.parser() owns its keys: they are kept as given (PR #687 local Codex run 2).
      Throws:
      ConfigWriteRefusedException - if several keys at one level read back to the same key; nothing is written
    • spelledKey

      private String spelledKey(Field field, List<String> above, String key, Type keyType, Object moduleLevel, Object fileLevel) throws ConfigWriteRefusedException
      One key of spelledLeaf(java.lang.reflect.Field, java.util.List<java.lang.String>, java.lang.Object): above holds the keys already resolved, outermost first.
      Throws:
      ConfigWriteRefusedException
    • keysReadingBack

      private List<String> keysReadingBack(Object level, Object typed, Type keyType, List<String> location)
      The keys of level, when it is a map, that read back through keyType to typed, in map order.
    • readKey

      private Object readKey(String text, Type keyType, List<String> location)
      text read as a map key of keyType on the loader's read path, or null when it does not convert. A probe: nothing is logged or recorded.
    • several

      private static boolean several(List<?> values)
    • severalSpellingsReason

      private String severalSpellingsReason(Field field, List<List<String>> leaves)
      The refusal text for typed map keys the file writes in several spellings, such as 1: and '01': (#673, case 52): the setting and each key (its first spelling) through describeKey(java.lang.reflect.Field, java.util.List<java.lang.String>), never a value or the other spellings.
    • severalSpellings

      private List<List<String>> severalSpellings(ConfigDocument read, Field field)
      Every typed map key that read holds for field in several spellings, relative to the setting and ending in its first spelling in file order (#673, case 52). Decided from the file's keys alone through the converter's file-key ambiguity scan - the same key index the writer binds keys with - so a key the module removed or never held still counts. A setting with its own legacy ConfigEntry.parser() has none: its parser, not the default key converter, decides which file keys are one key (PR #687 local Codex run 2).
    • registryConverts

      private static boolean registryConverts(Field field)
      Whether the converter registry converts field, as toPlainResult decides: an explicit non-default legacy ConfigEntry.parser() keeps conversion ownership, so no default typed-key spelling rule applies to it.
    • refuseSeveralSpellings

      private void refuseSeveralSpellings(ConfigDocument read, Field field) throws ConfigWriteRefusedException
      An operator write of a setting whose typed map keys read holds in several spellings is refused.
      Throws:
      ConfigWriteRefusedException
    • reportedSeveralSpellings

      private boolean reportedSeveralSpellings(ConfigDocument read, Field field, List<List<String>> leaves)
      The save rule for a setting whose typed map keys read holds in several spellings: none of its changed leaves is written, and one warning names them with the reason, never a value.
      Returns:
      whether the setting was reported and must not be written
    • requireOperatorWriteThread

      private void requireOperatorWriteThread(String operation)
    • declaredEntry

      private Field declaredEntry(String entryPath)
      The field declared at entryPath (ConfigEntry.path(), or the field name for an empty path).
    • writeOperatorChanges

      private void writeOperatorChanges(List<AbstractConfigEntity.ModuleChange> changes) throws IOException
      Writes an operator's explicit changes through the configuration write gate - the module's value at each change's path, whatever the file holds there now (the operator's consent), each owned as a whole key - against one read of the file, and advances the baseline and the last-read entry of exactly those paths. A refusal throws ConfigWriteRefusedException; nothing is acknowledged then.
      Throws:
      IOException
    • stageOperatorChanges

      private OperatorFileWriter.Staged stageOperatorChanges(List<AbstractConfigEntity.ModuleChange> changes) throws IOException
      Prepares an operator's explicit changes for the configuration write gate and stages the verified file (see writeOperatorChanges(java.util.List<com.ultikits.ultitools.abstracts.AbstractConfigEntity.ModuleChange>)); a write the gate settles as refused, or a file that cannot be read or parsed, throws ConfigWriteRefusedException here, before anything is staged.
      Throws:
      IOException
    • refused

      private ConfigWriteRefusedException refused(String reason)
    • presenceRefused

      The refusal of a map-entry write whose presence condition did not hold: logged at FINE only, the caller reports it (#623).
    • panelChanges

      private List<AbstractConfigEntity.ModuleChange> panelChanges(List<Field> touched, Map<Field,List<List<String>>> leaves)
      A panel edit's changes: each touched setting as a whole, or - for a map setting edited leaf by leaf - each touched leaf, at the value the edit applied. The operator named exactly these in the panel (maintainer decision of 2026-10-04: write exactly the item the operator explicitly asked to change).
    • warnNotWritten

      private void warnNotWritten(List<String> keys, String reason)
      Parameters:
      reason - why nothing was written, or null for the save rule's "not the value made from"
    • noteFailedWrite

      private void noteFailedWrite(IOException failure)
      Records that a write to this file could not be put back after it failed part-way (#622), so the file is treated as changed since it was read until a load reads it again; any other failure left the file holding its previous bytes.
    • registry

      private ConverterRegistry registry()
    • fieldPath

      private String fieldPath(Field field)
    • keys

      private List<String> keys(Field field)
    • keysIn

      private List<String> keysIn(ConfigDocument doc, Field field)
      Where doc holds field's setting: its one reading (ConfigDocument.readings(String)) - the nested keys, a flat dotted key such as features.chat, or a mix such as a.b: {c: 1} - or, when it holds none, where an insert puts it (insertKeysIn(com.ultikits.ultitools.config.document.ConfigDocument, java.lang.reflect.Field); the nested keys when that cannot be decided, for a caller that only asks whether the setting is held). One key semantics for every reader and writer of a declared setting (#612): a setting the operator wrote in flat form is read, saved and edited on that line, and never gets a nested copy. A setting held in several forms is refused first (heldTwice(com.ultikits.ultitools.config.document.ConfigDocument, java.lang.reflect.Field)).
    • insertKeysIn

      private List<String> insertKeysIn(ConfigDocument doc, Field field)
      Where an insert puts field's setting, which doc does not hold (#614, maintainer decision 2026-10-06): below the longest prefix of its path the file holds as a section (ConfigDocument.heldSections(java.lang.String)), the rest of the path as nested keys - so a section the operator wrote in flat dotted form (a.b:) gets the setting in place, never a second, nested copy of the section. When the file holds that section in several forms, the insert goes below the one form that holds another declared setting of the section (insert-location rule, revision 0, plan 17-75); when none or more than one does, the framework cannot tell, and this returns null: the insert is refused, naming the file and the setting (unplacedReason(com.ultikits.ultitools.config.document.ConfigDocument, java.lang.reflect.Field)). The nested keys of the whole path when the file holds no prefix as a section.

      A sibling written as one dotted key below the section (insert-location rule revision 1, gate-1 F4 of plan 17-75, maintainer decision 2026-10-06). When a declared setting of the same section is held with the rest of its path as one dotted key (a: / b.c: 3), it shares a longer prefix with the missing setting (a.b.d) than any section the file holds; the missing setting is then written in that sibling's form, beside it (b.d: 2), never as a nested b: / d: - a second form of a.b, which Bukkit reads as shadowing the first. When siblings sharing that prefix are written in different splits, it cannot be decided.

      Returns:
      the key path to insert at, or null when it cannot be decided
    • placement

      private AbstractConfigEntity.Placement placement(ConfigDocument doc, Field field)
    • mirroredPath

      private static List<String> mirroredPath(List<String> reading, int common, List<String> parts)
      The missing setting's path in a sibling's form: the sibling's reading cut at the common dotted segments both paths share, when that cut falls inside one of its keys ([a, b.c] cut after a.b gives [a, b.d] for a.b.d); null when the cut falls between keys (that prefix is a held section).
    • sectionHoldingASibling

      private List<String> sectionHoldingASibling(ConfigDocument doc, Field field, List<List<String>> sections)
      Of several forms of one section, the one form holding another declared setting of it; null for none or several.
    • unplacedReason

      private String unplacedReason(ConfigDocument doc, Field field)
      Why field's setting cannot be inserted (insertKeysIn(com.ultikits.ultitools.config.document.ConfigDocument, java.lang.reflect.Field) returned null): names, never values.
    • heldTwice

      private boolean heldTwice(ConfigDocument doc, Field field)
      Whether doc holds field's setting in more than one form (#612).
    • heldTwiceReason

      private String heldTwiceReason(Field field)
      The refusal text for a setting held in more than one form: the setting path, never a value (#612).
    • declaredType

      private Type declaredType(Field field)
    • plainValue

      private Object plainValue(Field field)
    • plainValue

      private Object plainValue(Field field, boolean writing)
      The plain value of field as the module holds it, spelled like the file this entity last read or wrote: the raw acknowledgement is passed as the file value, so a typed map key the file spells '01', 1.0 or in upper case keeps that text (#673, case 52). Every save, operator write, panel edit, unsaved-entry check and reload's in-memory side therefore names the operator's own key, which is the document's key identity.
    • plainValue

      private Object plainValue(Field field, boolean writing, Object fileValue)
      Parameters:
      fileValue - the plain value the file holds at the setting, whose typed map keys name the text each key is written under; null for canonical key text (a setting the file does not hold)
    • currentPlain

      private Map<Field,Object> currentPlain(List<Field> fields)
    • currentPlain

      private Map<Field,Object> currentPlain(List<Field> fields, boolean writing)
    • captureDefaults

      private void captureDefaults()
    • acknowledgeRaw

      private void acknowledgeRaw(ConfigDocument source, List<Field> fields)
    • isTokenComment

      private boolean isTokenComment(Field field)
    • resolvedComment

      private String resolvedComment(Field field)
    • addEntryComment

      private boolean addEntryComment(ConfigDocument target, Field field)
      Writes the framework's comment above field's key in target: for a token comment, only the run of comment lines frameworkCommentRun(com.ultikits.ultitools.config.document.ConfigDocument, java.lang.reflect.Field) identifies as the framework's is replaced, and a comment that is the operator's is left as it is; a literal comment is written only for a key just inserted, which has no comment yet.
      Returns:
      whether a comment line changed
    • frameworkCommentRun

      private int frameworkCommentRun(ConfigDocument target, Field field)
      How many of the last comment lines above field's key in target the framework wrote (#604, maintainer decision 2026-10-04: "only the framework's own comments are rewritten"). The key's comment - in the byte form it is written in (ConfigDocument.blockCommentAsWritten(List)), without the blank lines above it - is the framework's when it equals, as a whole or as its trailing run of lines, the exact form the framework writes (the key's column, "# " and the text; identification revision 1, 17-64 review round 1 R1-02) of the token's text in a catalogue the module's jar ships, of a text an earlier module version shipped for the entry (ConfigEntry.previousComments()), of the text an operator's custom language file in the module's lang/ folder holds for the token now (#615 item 1), of the text the module resolves now, or of the bare {key} token. Equality is the only test: no prefix, similarity, spacing or language tolerance, so a note the operator wrote above the framework's lines, a framework comment the operator edited, or the framework's text written at another column or without the space after # is never taken in. Of several matching texts the longest run counts, so a whole-comment match comes first.
      Returns:
      the run's length; 0 when the key has no comment (the framework's comment may be inserted); -1 when the comment is the operator's and must not be touched
    • frameworkRenderings

      private List<List<String>> frameworkRenderings(Field field)
      Every rendering of a token comment the framework may have written above field's key: the token's text in each catalogue the module's jar ships (read without any language-file side effect), the texts earlier module versions shipped for the entry (ConfigEntry.previousComments()), the token's text in each of the operator's custom language files in the module's lang/ folder (UltiToolsPlugin.customCatalogueTexts(java.lang.String), #615 item 1), the text the module resolves now, and the bare token - each in the byte form ConfigDocument.setFrameworkComment(java.util.List<java.lang.String>, java.util.List<java.lang.String>) writes it, as ConfigDocument.blockCommentAsWritten(List) reports it. An empty text is not a rendering: the framework writes no comment for it, so it can never identify an operator's bare # line.

      Why the custom files' texts cannot take in an operator's own comment. While a custom language is selected, the framework renders a token comment from that file's text for the same key, so a comment equal to it, above that key, is a text the framework wrote there; the operator supplied it through their own catalogue, for this very purpose. Only the text for the comment's own key counts, and only as the whole comment or its trailing run of lines, with no tolerance. A comment written from an earlier version of that text - edited in the custom file since - equals no current text and stays the operator's: no record of earlier renderings is kept (maintainer decision of 2026-10-06, option A). The custom files are only read; nothing records, refreshes or backs them up.

    • renderedComment

      private static List<String> renderedComment(String text)
      The comment lines text becomes when the framework writes it, in their written byte form.
    • updateTokenComments

      private boolean updateTokenComments(ConfigDocument target)
    • panelComment

      private String panelComment(Field field)
    • protectFailedLoad

      private boolean protectFailedLoad(ConfigLoadResult loaded)
    • failedLoadCause

      private static String failedLoadCause(ConfigLoadResult loaded)
      The safe cause of a load that could not read or parse the file, or null when it did. Names only the exception class or the parser's numeric location, never file content.
    • reloadRefusal

      private ConfigurationException reloadRefusal(ConfigLoadResult loaded, String cause)
      #589: a reload that cannot read or parse the file refuses with the file and the safe cause. The caller reports it (the module's reload logs one SEVERE line and /ul reload <name> replies failure), so the entity logs nothing itself.
    • safeParserLocation

      private static String safeParserLocation(String message)
    • configEntryFields

      private List<Field> configEntryFields()
      Every @ConfigEntry field this class declares or inherits, in ReflectionUtil.getFields(Class) order.
      Returns:
      the annotated fields, possibly empty
    • validateLoaded

      private void validateLoaded(boolean initialize)
      Runs validateFields() on the values a load just bound. A reload's refusal says that fixing the file and reloading again is enough (#595); a refusal at load keeps its wording.
    • isModifiedSinceSnapshot

      @Internal public final boolean isModifiedSinceSnapshot()
      Whether serialized fields differ from their last bound/persisted effective values - module changes not yet saved. Map iteration order is significant here; the storage equality used for no-op saves is not. Nothing writes them at server stop: they are reported, by key, and dropped (maintainer decision 2026-10-04). Protected files and uninitialized entities report false.
      Returns:
      whether the entity holds module changes that were never saved
      Since:
      6.3.0
    • unsavedEntryPaths

      @Internal public final List<String> unsavedEntryPaths()
      Names unsaved declared entries without exposing their values, for superseded-copy reporting.
      Returns:
      changed entry paths
    • orderedEquals

      private static boolean orderedEquals(Object left, Object right)
    • isPresentInFile

      public final boolean isPresentInFile(String path)
      Reports presence in the last successfully loaded document, including explicit nulls and undeclared keys. The path is read the way a declared setting path is (#612): split at its dots as nested keys, or held as a flat dotted key such as features.chat, or any mix of the two; the key is present when the file holds it in any of these forms. Failed loads report no presence.
      Parameters:
      path - dotted configuration path
      Returns:
      whether the last load contained the key
      Since:
      6.3.0
    • isLastLoadUnparseable

      @Internal public final boolean isLastLoadUnparseable()
      Whether the last attempt to read this configuration's file failed to parse (#510) - in practice, whether the file on disk holds invalid YAML. Nothing writes configuration at server stop as of 6.3.0; the stop report names such a configuration once as left alone, instead of listing unsaved keys, and no write path ever replaces a file the framework could not read.

      Framework-internal: this method is called only by ConfigManager's stop and unload report and is public solely because ConfigManager lives in another package. Module code should not call it.

      Returns:
      true if the last load of this configuration's file failed to parse, and no successful load has happened since
      Since:
      6.3.0
    • isLastInitIncomplete

      @Internal public final boolean isLastInitIncomplete()
      Whether the last init(com.ultikits.ultitools.abstracts.UltiToolsPlugin) failed before its field validation completed -- for example because writing back a missing key threw an IOException, which ConfigManager logs and continues past. The fields may then hold values that their validation annotations never checked. The config-bound @Scheduled/@CmdCD step (#531) does not apply values from such an entity: it refuses the module at load and keeps the running value on reload.

      Thread contract: the marker is set by init and read by the binding step on the main server thread, where the framework and the first-party modules run both. An init or reload() run off the main thread is not synchronized with the binding step; confining them to the main thread is tracked in UltiKits/UltiTools-Reborn#538.

      Framework-internal: public solely because the binding step lives in another package. Module code should not call it.

      Returns:
      true if the last init did not complete its validation
      Since:
      6.3.0
    • addBindingRange

      @Internal public final void addBindingRange(String key, String rule, Predicate<Long> isValid)
      Records that key is bound by a config-bound @Scheduled or @CmdCD (#531), so that a panel write setting it to a value isValid rejects is refused, naming the key, the value and rule. The binding step calls this when it resolves the binding at load. Recording the same rule twice for a key keeps one.

      Framework-internal: public solely because the binding step lives in another package. Module code should not call it.

      Parameters:
      key - the @ConfigEntry path, or the field name when the path is empty
      rule - the range, as a sentence, for the refusal message
      isValid - whether a value, in seconds (null for a null boxed field), is inside the range
      Since:
      6.3.0
    • isFileModifiedSinceSnapshot

      @Internal public final boolean isFileModifiedSinceSnapshot()
      Whether the file on disk differs from the file as it was at the last snapshot point (#510) - in practice, whether someone edited, replaced or removed it while the server was running. The snapshot is the bytes this entity last bound or wrote itself, never a fresh read, so an operator's edit stays visible here until the next load. It is also true while a write to the file failed part-way and could not be put back from its framework backup (#622): the file may then hold part of that write, so it is treated as changed since it was read until a load reads it again.

      Framework-internal: no production code calls it as of 6.3.0. Its former caller, ConfigManager#saveAll(), no longer writes or compares anything (#510, #599); the method is kept for the framework's own regression tests of the snapshot. It is public only because that former caller lives in another package. Module code should not call it.

      Returns:
      true if the file's fingerprint changed since the last snapshot, or a write to it could not be put back; false if neither, or if no snapshot has been taken yet
      Since:
      6.3.0
    • fingerprintOf

      private static String fingerprintOf(File file)
      Fingerprints a configuration file by the SHA-256 digest of its bytes (#510). A content hash rather than size plus modification time: a typical operator edit changes one value to another of the same length (60 to 90, true to TRUE), which size alone cannot see, and modification time is coarse or preserved on common paths (two-second resolution on FAT and many network shares, cp -p, rsync -t, editors that restore it). Configuration files are small and this runs only at load, reload and write time.
      Parameters:
      file - the configuration file, possibly null or missing
      Returns:
      "absent" for a missing file, "unreadable" if it cannot be read, or the Base64 SHA-256 digest of its contents
    • init

      public final void init(UltiToolsPlugin ultiToolsPlugin) throws IOException
      Initializes the configuration entity.
      Parameters:
      ultiToolsPlugin - the plugin instance
      Throws:
      IOException - if an I/O error occurs
    • initForBatch

      @Internal public final void initForBatch(UltiToolsPlugin plugin) throws IOException
      Framework manager bridge; binds without flushing initialization writes.
      Parameters:
      plugin - owning module
      Throws:
      IOException - on load failure
    • initialize

      private void initialize(UltiToolsPlugin ultiToolsPlugin, boolean deferred) throws IOException
      Throws:
      IOException
    • load

      private void load(boolean initialize) throws IOException
      Throws:
      IOException
    • bindDeclaredDefault

      private void bindDeclaredDefault(Field field, boolean deleted)
      Binds field's initially declared default because a reload found its key missing from the file and the module had not changed the setting since the last load or save (#596 item 2): deleting a key resets that setting, as an unusable value already does (#523). Memory only - the file is not changed, and a later save does not re-add the key. When the key was in the file at the previous load - the operator deleted it - one warning names the file and the key, never a value; a key the file already lacked (one a refused insert could not add, already named when that write was refused) is not named again at every reload.
      Parameters:
      field - the setting whose key is missing
      deleted - whether the previous load found the key in the file
    • warnGateFailure

      private void warnGateFailure(String action, RuntimeException failure)
      An unchecked failure inside the configuration write gate during start-up never refuses the module: one warning names the file and the failure's class (never a message, which may quote file content), the declared defaults run in memory, and the file is not changed (17-65 review round 2 R2-02).
    • expectedBase

      private static String expectedBase(ConfigLoadResult loaded)
    • tokenCommentsDiffer

      private boolean tokenCommentsDiffer(ConfigDocument target)
      Whether a framework token comment in target differs from what the current language renders, without changing target: the framework's own run (frameworkCommentRun(com.ultikits.ultitools.config.document.ConfigDocument, java.lang.reflect.Field)) compared, in its written byte form, with the current rendering; a comment that is the operator's never differs.
      Parameters:
      target - the document as read
      Returns:
      whether a comment-only write would change a comment
    • ownFrameworkComments

      private void ownFrameworkComments(OwnedPaths.Builder owned, ConfigDocument read)
      The token comments of read the framework may rewrite, each owning only the run of lines frameworkCommentRun(com.ultikits.ultitools.config.document.ConfigDocument, java.lang.reflect.Field) identified as the framework's in the file as read (#604): the config write gate then refuses any write that changes a comment line above that run or of a comment that is the operator's.
      Parameters:
      owned - the ownership being built
      read - the document as read, holding the bytes the write is checked against
    • rewriteTokenComments

      private OperatorFileWriter.Result rewriteTokenComments(ConfigLoadResult loaded)
      Rewrites the framework's token comments in the current language through the config write gate (OperatorFileWriter), at start-up and on reload. This cannot overwrite operator content: the write owns only the comment lines the framework identified as its own above token-commented keys (#604; an operator's comment, or the lines above the framework's run, are not owned), the gate verifies that every other byte of the file is unchanged after rendering and writes nothing when the file no longer holds the bytes loaded read, and it refuses a file using anchors. A refusal or an I/O failure logs one warning and changes no save state, so no later save follows from it (#603); nothing is written at stop.
      Parameters:
      loaded - the load being bound (LOADED)
      Returns:
      the gate's result, or null after an I/O failure
    • refreshFrameworkComments

      @Internal public final void refreshFrameworkComments()
      Rewrites this configuration's framework comment lines in the owning module's language as it is now (#594).

      A reload reads the module's configurations before it rebuilds the module's language, so the comment pass of that read resolved the tokens with the catalogue of the language the module ran with until then. UltiToolsPlugin's reload calls this, through ConfigManager, right after the rebuild and before the module's own reload hook, so a language switch applied by /ul reload reaches the comments too (maintainer decision 2026-10-04, "the framework only refreshes comments on reload").

      Why it cannot overwrite operator content. It reads the file afresh and hands the config write gate (OperatorFileWriter) a comment-only write that owns just the comment lines the framework identifies as its own above token-commented keys (frameworkCommentRun(com.ultikits.ultitools.config.document.ConfigDocument, java.lang.reflect.Field), #604), checked against the bytes of that fresh read: no value, no key and no other comment line can change, so an operator's invalid value, a key deleted to reset it, a hand-written note and an edit saved after the reload read the file all stay as typed, and a file that changed again before publishing, uses anchors or has a layout the renderer would normalize is refused with the gate's one warning. Nothing is attempted when the last load refused the file as unreadable or unparseable (it is never written), while a first-start write is still pending, or when no framework comment differs from the current language, nor when this reload's own load already attempted a comment write that was refused or failed: that attempt logged the file's one warning for this reload, and the next reload tries again (17-64 review round 1 R1-03). A refusal or a failure logs one warning and changes no save state, so no later save follows from it (#603, #597 review F2); nothing is written at stop. Only when the fresh read still held exactly the bytes this entity bound does the entity record what the gate wrote as its last read; otherwise the operator's newer file stays a change on disk.

      Framework-internal: public solely because ConfigManager lives in another package. Module code should not call it.

      Since:
      6.3.0
    • writeInitialization

      private OperatorFileWriter.Result writeInitialization(Map<Field,Object> inserted, String expected, ConfigDocument read) throws IOException
      Writes an initialization - the declared keys init found missing, each with its comment, and the framework's token comments - through the config write gate (OperatorFileWriter), at once or from the batch flush. This cannot overwrite operator content: the write owns only the inserted keys and the token comment lines the framework identified as its own (#604) (or the whole file when it was absent, created exclusively so a file that appeared meanwhile is never replaced) - and, for a key whose section the operator left with no value (messages: after deleting every key of the section), that section's one line, which carried no value and keeps its key text and comment (#620, maintainer decision 2026-10-06: the key is put back so the section is complete) - the gate verifies that every other byte of the file is unchanged after rendering (layout included), and it writes nothing when the file no longer holds the bytes read at expected. When it does not write, the gate has logged one warning naming the file and the keys, the fields keep their declared defaults in memory, and the file keeps its bytes.
      Parameters:
      inserted - the missing fields and their declared default values, as plain data
      expected - the fingerprint of the bytes the init read, or OperatorFileWriter.ABSENT
      read - the document the init read from those bytes
      Returns:
      the gate's result; when it applied the edit, its document and fingerprint are what the file holds
      Throws:
      IOException - if publishing the verified text fails
    • entityFingerprint

      private static String entityFingerprint(String sha256Hex)
      The fingerprint form fingerprintOf(java.io.File) uses, for a storage-layer SHA-256 in lower-case hex (or OperatorFileWriter.ABSENT): what the entity records as the bytes it last bound or wrote (#602).
    • mergeReload

      private Object mergeReload(Object base, Object mine, Object theirs, Type declared, Field field, List<String> leaf, List<String> conflicts)
      The #511 three-way reload merge of one setting. Only a value declared as a Map is merged key by key, following the declared value types into nested maps; any other value - a list, a ConfigurationSerializable such as a Bukkit Location or Vector, a typed map's value that is not a map - is one value: the module's, the file's, or on a conflict the file's whole, never a value mixed from both (17-65 review round 2 R2-01, the same rule as the save's changedLeaves(java.lang.Object, java.lang.Object, java.lang.reflect.Type, java.util.List<java.lang.String>, java.util.List<java.util.List<java.lang.String>>)). A conflict over a composite value or a list names the key only.

      The recursion carries the setting and the real keys down to the value. A conflict's key is displayed through describeKey(java.lang.reflect.Field, java.util.List<java.lang.String>), so a key below a secret-shaped field name, declared path segment or map key is never named, and whether the discarded value is redacted is decided from the real keys (#673, case 54). The merged map keeps the real keys.

    • flushInitializationWrite

      @Internal public final void flushInitializationWrite() throws IOException
      Flushes only a validated manager batch's initialization write, through the config write gate and only while the file still holds the bytes initForBatch read (#602); see writeInitialization(java.util.Map<java.lang.reflect.Field, java.lang.Object>, java.lang.String, com.ultikits.ultitools.config.document.ConfigDocument).
      Throws:
      IOException - when replacement fails; the entity is then protected until reload
    • discardInitializationWrite

      @Internal public final void discardInitializationWrite()
      Clears an uncommitted initialization belonging to a refused manager batch.
    • warnConversion

      private void warnConversion(Field field, ConversionException failure, Object raw, Object fieldRaw)
    • conversionReason

      private static String conversionReason(ConversionException failure)
    • containsSecret

      private boolean containsSecret(Object value)
    • typeName

      private static String typeName(Type type)
    • splitComment

      private static List<String> splitComment(String comment)
      Splits a @ConfigEntry.comment() value into one List element per line, in declaration order, ready for ConfigurationSection's comment-writing API. No blank leading element is added (Claude's Discretion, D-07) - it would produce a diff on every regenerated file for a purely cosmetic gain, contrary to D-01's touch-as-little-as-possible posture.
      Parameters:
      comment - the raw comment() attribute value, possibly empty
      Returns:
      one element per line, or an empty list if comment is blank
    • updateProperties

      public void updateProperties(com.google.gson.JsonObject jsonObject) throws IOException
      Updates the properties of the configuration entity.

      The field traversal and path-derivation must match init() / save() / reload() exactly, or a write can silently no-op: all four methods walk @ConfigEntry fields, and this method previously diverged from them in two ways -- using getDeclaredFields() instead of ReflectionUtil.getFields(Class) (missing inherited fields), and not normalizing an empty path to the field name. The second divergence was the more hidden one: omitting path on @ConfigEntry is a supported style, init/save read and write it by field name, and toJsonObject() also sends it to the panel by field name -- but this method was looking for an empty-string key in the JSON, which never exists, so the field was silently skipped while persistence still ran and the caller still received success.

      Since 6.3.0 a panel edit writes exactly the settings it touches - or, inside a map setting, exactly the touched entries - through the framework's configuration write gate: the operator named them, so their values replace what the file holds there, and every other key, value, comment and byte of the file stays as it is (maintainer decision of 2026-10-04, "what code may write, by file type"). It cannot overwrite other operator content: the gate owns only the touched keys and writes nothing unless every other line comes out byte-identical. When the gate refuses (anchors, a layout it cannot keep, a file that cannot be read or parsed, or one that changed while the write was prepared), this throws ConfigWriteRefusedException naming the reason and the entity is left exactly as before the call.

      Since 6.3.0 (SILENT-14, closing CR-01) this method validates the full post-update field state - the same validateFields() init(UltiToolsPlugin)/reload() already use - before either document mutation or file persistence runs. A violating value refuses with ConfigurationException instead of being written: the operator's file is left byte-identical, and every field this call touched is restored to the value it held before the call, so memory never disagrees with disk (D-01, D-04). The entity keeps running after a refusal, so its in-memory state must not be left holding a rejected value; reload() follows the same all-or-nothing rule. The same holds when the file replacement itself fails: every field and every piece of save tracking is restored to its state before the call, and the IOException is rethrown.

      Parameters:
      jsonObject - the JSON object containing the new properties
      Throws:
      IOException - if an I/O error occurs, or the write gate refused the write (ConfigWriteRefusedException); the entity is then left as it was
      ConfigurationException - with ErrorCode.CONFIG_VALIDATION_FAILED if the post-update field state violates a @Range/ @NotEmpty/@Size/@Pattern constraint - the file is not written and touched fields are restored
    • preparePanelWrite

      @Internal public final AbstractConfigEntity.PanelWrite preparePanelWrite(com.google.gson.JsonObject properties) throws IOException
      Prepares one panel entity without acknowledging or replacing its file: the touched settings or leaves are verified and staged through the configuration write gate (see updateProperties(com.google.gson.JsonObject)); the commit re-checks the file's bytes immediately before its move.
      Parameters:
      properties - proposed panel values
      Returns:
      manager-owned write
      Throws:
      IOException - if reading or staging fails, or the gate refused the write (ConfigWriteRefusedException)
    • restoreFields

      private void restoreFields(List<Field> fields, List<Object> values)
    • validateProposedProperties

      public void validateProposedProperties(com.google.gson.JsonObject jsonObject)
      Validates that applying jsonObject's touched fields would NOT violate any @Range/@NotEmpty/@Size/@Pattern constraint, without persisting anything to disk or leaving any field changed (#358 Part 2).

      Used by ConfigManager.loadFromJson(String) to validate every entity touched by a multi-file panel-pushed batch BEFORE persisting any of them - a refusal on entity N must not leave entities 1..N-1 already written to disk. This method always restores every field it touched, whether validation passes or fails; the caller is expected to call updateProperties(JsonObject) itself afterward (which re-validates - cheap on the documented idiom - and persists) once every entity in its own batch has passed this check.

      Parameters:
      jsonObject - the JSON object containing the candidate new properties
      Throws:
      ConfigurationException - with ErrorCode.CONFIG_VALIDATION_FAILED if the candidate post-update field state would violate a validation constraint
    • applyAndValidate

      private Map<Field,List<List<String>>> applyAndValidate(com.google.gson.JsonObject jsonObject, List<Field> touchedFieldsOut, List<Object> previousValuesOut)
      Applies every jsonObject field this class declares via @ConfigEntry to this instance, then validates the resulting state via validateFields(). Never itself persists or restores anything - callers decide what happens next. touchedFieldsOut/previousValuesOut are populated even when validateFields() throws, so a caller can still restore exactly what this call touched.
      Parameters:
      jsonObject - the JSON object containing the candidate new properties
      touchedFieldsOut - populated, in application order, with every field this call applied
      previousValuesOut - populated in the same order with each field's pre-call value
      Throws:
      ConfigurationException - with ErrorCode.CONFIG_VALIDATION_FAILED if the post-update field state violates a constraint. A refusal of panel paths names each path through the display rule of describeKey(java.lang.reflect.Field, java.util.List<java.lang.String>) - never a key below a secret-shaped one, never a value - because the message is logged and returned to the panel (#673, case 54)
    • describeUndeclared

      private String describeUndeclared(String path)
      How a refusal names a panel path no declared setting owns: the rule of displayKeys(boolean, java.util.List<java.lang.String>) over its dotted segments, so everything below the first secret-shaped segment is redacted (#673).
    • dottedKeys

      private static List<String> dottedKeys(String path)
      The dotted segments of a panel path whose real keys are not known, for display only: a whole key holding a dot is split, which can only redact more, never less (no secret-shaped word contains a dot).
    • describeReadings

      private List<List<String>> describeReadings(Field owner, List<List<String>> readings)
      Every reading of an ambiguous panel path, as the real keys of owner's map displayed by displayKeys(boolean, java.util.List<java.lang.String>).
    • describeFailure

      private String describeFailure(Field field, List<String> path)
      How a refusal names the path of a conversion failure of field: through describeKey(java.lang.reflect.Field, java.util.List<java.lang.String>) with the keys below the setting's declared keys; a path that does not start with them is displayed whole by the same rule.
    • parentInMemory

      private static boolean parentInMemory(Object tree, List<String> path)
      Whether the map holding the last key of path exists in tree (the module's value in memory).
    • isSerializedMap

      private static boolean isSerializedMap(Object value)
      Whether value is a serialized Bukkit value: a map carrying the == type key.
    • widenLike

      private static Object widenLike(Object value, Object guide)
      value with each whole number inside a serialized Bukkit value (isSerializedMap(java.lang.Object)) widened to a Double where guide - the module's own value, as plain data - holds a floating-point number at the same place; everything else as it is. A whole number given directly (guide a floating-point number) is widened too. Used for a panel edit's conversion and for the edited field it writes (#609, R4-I1).
    • widenLike

      private static Object widenLike(Object value, Object guide, boolean inSerialized)
    • mapLeaf

      private static Object mapLeaf(Object tree, List<String> path)
    • mapContains

      private static boolean mapContains(Object tree, List<String> path)
    • withoutLeaf

      private static Object withoutLeaf(Object tree, List<String> path)
      A copy of tree without the entry at path (a copy of tree when it has none).
    • patchedMap

      private static Object patchedMap(Object tree, List<String> path, Object value)
    • matchMapPaths

      private static void matchMapPaths(Object node, String remaining, List<String> prefix, List<List<String>> matches)
    • replaceMapLeaf

      private static void replaceMapLeaf(Object tree, List<String> path, Object value)
    • jsonToPlain

      private static Object jsonToPlain(com.google.gson.JsonElement element)
    • validateBindingRanges

      private void validateBindingRanges(List<Field> touchedFields)
      Refuses a panel write that sets a key bound by a config binding (#531) outside that binding's range, in the same shape as a @Range violation. Only the fields this write touched are checked: a bound field left as it was may hold a value a refused reload kept out of use, and editing another key must not fail because of it.
      Parameters:
      touchedFields - the fields this write applied
      Throws:
      ConfigurationException - with ErrorCode.CONFIG_VALIDATION_FAILED naming each bound key, its value and its range
    • toJsonObject

      public com.google.gson.JsonObject toJsonObject()
      Converts the configuration entity to a JSON object.
      Returns:
      the JSON object representation of the configuration entity
    • addLeaves

      private static void addLeaves(com.google.gson.JsonObject result, String prefix, Map<?,?> values, com.google.gson.Gson gson)
    • getComments

      public com.google.gson.JsonObject getComments()
      Gets the comments of the configuration entity.
      Returns:
      a JSON object containing the comments
    • validateFields

      protected void validateFields()
      Validates all fields annotated with validation annotations (@Range, @NotEmpty, @Size, @Pattern). A violation refuses this config's module instead of rewriting the value - the operator's file is never modified (D-01). Every violating field is collected and named in a single refusal. At load the operator fixes the value(s) on disk and restarts; on a reload the running values are kept and fixing the file and reloading again is enough (#595).
      Throws:
      ConfigurationException - with ErrorCode.CONFIG_VALIDATION_FAILED if any field violates its validation constraint, or if this config class cannot be constructed through either of the two framework-supported idioms (D-03)
    • ensureConstructable

      private void ensureConstructable()
      Constructs and discards an instance of this config class through the same two-step fallback ConfigManager.registerAll uses at registration time - a (String) constructor first, then an accessible no-arg constructor. Existence, not the constructed value, is what this proves: the framework needs every registered config class to still be buildable through one of its two documented idioms (D-02). Neither resolving is a genuine config-class error (D-03).
      Throws:
      ConfigurationException - if neither constructor resolves
    • constructSibling

      private AbstractConfigEntity constructSibling()
      Constructs a fresh instance of this config class through the (String) constructor, or failing that the no-arg constructor - the two idioms ensureConstructable() proves - for the existing validation constructability precondition only.
      Returns:
      a new, uninitialized instance of this entity's class
      Throws:
      ConfigurationException - if neither constructor resolves
    • validateSingleField

      private String validateSingleField(Field field) throws IllegalAccessException
      Describes the single validation constraint field violates, if any.
      Parameters:
      field - the field to check, already made accessible
      Returns:
      a violation description naming the field, its actual value (redacted for @Pattern on a secret-shaped field name, T-04-04) and the broken constraint, or null if the field's value satisfies its annotations
      Throws:
      IllegalAccessException
    • isSecretShapedFieldName

      private boolean isSecretShapedFieldName(String fieldName)
      Whether a field name looks like it stores a secret. Only @Pattern violations echo an arbitrary string value; @Range/@Size violations always echo a number or a length, and @NotEmpty violations are empty by definition, so neither can leak a secret verbatim (T-04-04).

      Widened in 6.3.0 (04-REVIEW.md WR-03) to also cover key/auth/ private/cert, accepting the resulting false positives (a field merely named e.g. publicKey is redacted too) as the safer default, per Phase 2 D-15's fail-closed preference. The reason for the widening is new, not cosmetic: before the write-path refusal added by this same 6.3.0 change, a @Pattern refusal message went only to the local console; now both remote config-write handlers forward it verbatim to UltiPanel over the WebSocket (T-04-56), so a name-heuristic miss here leaks the server, not just the console.

    • unreadableKind

      private String unreadableKind(Field field, Object value)
      A violation for a bound value of a kind the field's annotation cannot read - possible only when the setting is declared as a supertype such as Object (final review of plan 17-76: a declared constraint works or is refused, never passes silently). @Range reads numbers, @Pattern text, @Size text, collections, maps and arrays, @NotEmpty the same kinds; null is left to @NotEmpty as before. It is a violation like any other, with that annotation's outcome (refused at load; at reload the reload is refused and the running values are kept). The message names the field and the value's kind, never the value.
      Returns:
      the violation, or null when every annotation on the field can read the value
    • isRangeViolation

      private boolean isRangeViolation(Field field, Object value)
    • isNotEmptyViolation

      private boolean isNotEmptyViolation(Field field, Object value)
    • isSizeViolation

      private boolean isSizeViolation(Field field, Object value)
    • isPatternViolation

      private boolean isPatternViolation(Field field, Object value)
    • getValueLength

      private int getValueLength(Object value)
      What @Size counts: a text's length (a char is one character), a collection's size, a map's entries and an array's length (maps and arrays since 6.3.0, #631); -1 for anything else.
    • containerKind

      private String containerKind(Field field)
      The value kind @NotEmpty treats as a container on field: "list" (a list or an array), "set", "map" or "collection" by the field's declared type, or null for any other type (text keeps its refusal).
    • isEmptyContainer

      private static boolean isEmptyContainer(Object value)
      Whether a container value holds nothing: null, an empty collection, map or array.
    • useDeclaredDefaultIfEmpty

      private void useDeclaredDefaultIfEmpty(Field field, Object raw)
      A @NotEmpty list, set or map that a load or reload bound empty - the file holds it empty or null, or every entry failed to bind - runs on the field's declared default in memory (#630, maintainer decision of 2026-10-06). One WARNING names the file, the key, the value kind, the value as written and the default; both are redacted when the field name, a key segment, or a map key inside the value or the default is secret-shaped, as a conversion warning is. It is not a violation, so the module loads.

      Why it cannot overwrite operator content: nothing here writes. The setting's baseline becomes the default the field now holds (the load records the bound value after this call) and the file's value stays the one last read, so save() finds no module change to write and never puts the default over the operator's empty value.

      Parameters:
      field - the setting just bound, already made accessible
      raw - the value as the file holds it
    • oneLine

      private static String oneLine(boolean redacted, Object value)
      A value for a warning on one log line, or <redacted>.
    • checkConstraintDeclarations

      private void checkConstraintDeclarations()
      Refuses, at load and before the file is read, a constraint declaration the framework cannot hold (maintainer decision of 2026-10-06, option A; #630, #631). One refusal names every such field; no file is read or written:
      • a constraint on a value type it can never check: @Range off numbers, @Pattern off text, @Size and @NotEmpty off text, collections, maps and arrays (text is a String or a char);
      • a constraint on a field of this class that is not a @ConfigEntry setting;
      • a @NotEmpty list, set, map or array whose declared default is empty, or outside the field's own @Size, which leaves nothing valid to run in place of an empty value.
      Only this class's own fields are read, each by its declared type through ConversionTypes.raw(java.lang.reflect.Type): the check never walks into a setting's value type (maintainer decision of 2026-10-06, the simplest route). A constraint annotation on a field of a value type, a nested class or anything a converter produces is never checked and not reported - the documented limit (COMPATIBILITY.md); a module validates such fields in its converter.
    • defaultCannotStandIn

      private String defaultCannotStandIn(Field field, String kind)
      Why the declared default of a @NotEmpty list, set, map or array cannot stand in for an empty value, or null when it can (#630, maintainer decision of 2026-10-06): it is empty itself, or it violates the field's other constraint - @Size, the only other one such a field can carry (the declaration check refuses @Range and @Pattern on it).
    • constraintsOn

      private static List<Annotation> constraintsOn(AnnotatedElement element)
      The four constraint annotations element carries, in a fixed order.
    • unsupportedBy

      private static String unsupportedBy(Annotation constraint, Class<?> raw)
    • canHold

      private static boolean canHold(Class<?> raw, List<Class<?>> kinds)
      Whether a field declared as raw can hold a value of one of kinds: raw is one of them, a subtype, or a supertype such as Object, Serializable, Comparable, CharSequence or Number (final review F1 of plan 17-76). The runtime validator checks the value bound at load, so only a declared type that can never hold a checkable value is a declaration error.
    • isNumberType

      private static boolean isNumberType(Class<?> raw)
    • isTextType

      private static boolean isTextType(Class<?> raw)
    • isContainerType

      private static boolean isContainerType(Class<?> raw)
    • addChangeListener

      public void addChangeListener(ConfigChangeListener listener)
      Adds a configuration change listener. The listener will be notified when the configuration is reloaded.
      Parameters:
      listener - the listener to add
    • removeChangeListener

      public void removeChangeListener(ConfigChangeListener listener)
      Removes a configuration change listener.
      Parameters:
      listener - the listener to remove
    • clearChangeListeners

      public void clearChangeListeners()
      Removes all configuration change listeners.
    • getChangeListenerCount

      public int getChangeListenerCount()
      Gets the number of registered change listeners.
      Returns:
      the number of listeners
    • notifyChangeListeners

      protected void notifyChangeListeners()
      Notifies all registered listeners about the configuration change. Individual listener exceptions do not affect other listeners.
    • reload

      public void reload() throws IOException
      Reloads the configuration from file and notifies all listeners.

      Since 6.3.0 a reload is all-or-nothing: if it fails for any reason (a validation violation, a conversion or I/O failure), every field value and every piece of load tracking is restored to what it was before the attempt, and the failure is rethrown without notifying listeners. A rejected file value therefore never becomes an unsaved in-memory edit that the next reload's three-way merge would keep over a corrected file.

      Since 6.3.0 (#589) a file that cannot be read or parsed fails the reload the same way: it throws ConfigurationException naming the file and the safe cause (the exception class, or the parser's line and column - never file content), the running values and the file stay exactly as they were, and the file stays protected from writes until a later successful load. Nothing is logged by the entity for it: the caller reports the failure, as /ul reload does. Initial loading through init(com.ultikits.ultitools.abstracts.UltiToolsPlugin) is different and unchanged: it logs one SEVERE line, keeps the declared defaults, and never overwrites the file.

      Throws:
      ConfigurationException - if the file cannot be read or parsed, or a value it holds fails validation or conversion; nothing changed
      IOException - if an I/O error occurs