Class UltiToolsPlugin

java.lang.Object
com.ultikits.ultitools.abstracts.UltiToolsPlugin
All Implemented Interfaces:
Configurable, IPlugin, Localized

public abstract class UltiToolsPlugin extends Object implements IPlugin, Localized, Configurable
Abstract class representing a plugin module.
Version:
1.0.0
Author:
wisdomme
  • Field Details

    • LANGUAGE_EXTENSIONS

      private static final String[] LANGUAGE_EXTENSIONS
      Language file extensions, in the order they are tried.

      .json stays first so a module shipping both keeps exactly the behaviour it had before 6.3.0. .yml and .yaml were added for #389: eight modules ship YAML, and the loader silently produced an empty dictionary for every one of them.

    • LOGGER

      private static final Logger LOGGER
      Named logger for the D-03 per-module reload line -- see RELOAD_LOG_MESSAGE_KEY.
    • RELOAD_LOG_MESSAGE_KEY

      static final String RELOAD_LOG_MESSAGE_KEY
      Framework i18n key (this class's own lang/en.json/lang/zh.json catalogue, not a module's) for the per-module reload line reloadSelf() logs after its three steps (D-03). Package-private so UltiToolsPluginLifecycleHookTest can assert both shipped catalogues actually carry a translation for it, rather than duplicating the literal string between production and test code.
      See Also:
    • PLACEHOLDER_PATTERN

      private static final Pattern PLACEHOLDER_PATTERN
      Matches a java.util.Formatter conversion specifier, e.g. %s in "Hello, %s!", or %1$s for an explicit argument index. Used only by placeholderArity(String), one of the two placeholder-loss detectors applyPlaceholderArityOverride(java.io.File, byte[], java.lang.String, java.lang.String) combines (see that method's own javadoc) for the D-05 per-key comparison; not a change to how placeholders are substituted anywhere -- every parameterised i18n(...) value in THIS FRAMEWORK's own two catalogues ( src/main/resources/lang/en.json/zh.json) is formatted with String.format, never MessageFormat (measured: 0 {n}-style placeholders in either file, and 0 across any i18n(...) call site in this repository).

      That measurement is scoped to THIS repository's own two catalogues -- it says nothing about the module catalogues this same mechanism actually protects. Corrected 2026-09-22: an earlier revision of this javadoc reported the same 0-count without that scoping qualifier, which read as "no {n}-style placeholder exists anywhere this mechanism looks" and was wrong for the broader population the mechanism serves. An install-wide sweep of all 15 module jars' lang/*.json/lang/*.yml catalogues against their on-disk copies (34 file pairs) found the opposite: 0 %s/%d-arity mismatches anywhere, and every brace-delimited span in that corpus is a {0}/{1}-style positional or {PLAYER}/{WORLD}-style named token -- the modules use String#replace-driven substitution exclusively, never String.format. BRACE_TOKEN_PATTERN and missingBracePlaceholder(java.lang.String, java.lang.String) exist to cover that population; this pattern and placeholderArity(java.lang.String) continue to cover only the framework's own.

      Codex round 6, P2: deliberately narrower than the full Formatter grammar -- %s/%d (optionally explicit-indexed) or a literal %%, with NO flags, width or precision component. The earlier, more permissive pattern accepted a space as a flag character, so ordinary text like "Progress: 90% done" matched "% d" as a (bogus) %d conversion with a space flag -- an operator's customised value containing an innocent '%' could be reported as an arity mismatch against differently-worded bundled text and silently overwritten at runtime, even though the key carries no real String.format placeholder at all. Since this framework never emits anything outside %s/%d/%% (per the measurement above), restricting the pattern to exactly those forms eliminates the false-positive class rather than patching the specific "% d" instance -- no genuine placeholder this framework uses stops matching.

    • BRACE_TOKEN_PATTERN

      private static final Pattern BRACE_TOKEN_PATTERN
      Matches a brace-style placeholder token as used by the MODULE lang catalogues this mechanism protects -- {0}/{1} positional or {PLAYER}/ {WORLD} named, substituted with plain String#replace, never java.util.Formatter. See PLACEHOLDER_PATTERN's javadoc for the install-wide measurement establishing this is the modules' actual dialect, and #524 for the real-machine report this pattern and missingBracePlaceholder(java.lang.String, java.lang.String) exist to fix.

      The captured group requires the braces to contain ONLY a bare decimal integer or a bare identifier (a letter/underscore followed by letters, digits or underscores) -- no whitespace or other punctuation. This is deliberately strict, for the same reason PLACEHOLDER_PATTERN was narrowed in Codex round 6: an operator's own decorative or explanatory text that happens to contain a brace must never be mistaken for a placeholder token, and this pattern alone cannot make that guarantee -- missingBracePlaceholder(java.lang.String, java.lang.String)'s one-directional comparison is what actually protects a decorative brace the disk value adds on its own (see that method's javadoc). Measured against the install-wide sweep above: every brace-delimited span across all 34 module file pairs matches this pattern with nothing left over, so this shape is not hand-picked -- it is what the corpus actually contains.

    • ARITY_GSON

      private static final com.google.gson.Gson ARITY_GSON
      A private, independent JSON reader for the D-05 placeholder-arity comparison only -- not a change to Language's own Gson usage, which stays entirely inside Language.java (untouched by this plan).
    • ARITY_MAP_TYPE

      private static final Type ARITY_MAP_TYPE
    • language

      private Language language
    • version

      private final String version
    • pluginName

      private final String pluginName
    • authors

      private final List<String> authors
    • loadAfter

      private final List<String> loadAfter
    • minUltiToolsVersion

      private final int minUltiToolsVersion
    • mainClass

      private final String mainClass
    • identifyString

      private final String identifyString
    • resourceFolderPath

      private String resourceFolderPath
    • context

      private SimpleContainer context
    • dataScope

      private DataScope dataScope
  • Constructor Details

    • UltiToolsPlugin

      protected UltiToolsPlugin()
      Constructor for UltiToolsPlugin. For module development only.
    • UltiToolsPlugin

      public UltiToolsPlugin(String pluginName, String version, List<String> authors, List<String> loadAfter, int minUltiToolsVersion, String mainClass, String resourceFolderPath)
      Constructor for UltiToolsPlugin. For plugin connector.
      Parameters:
      pluginName - the name of the plugin
      version - the version of the plugin
      authors - the authors of the plugin
      loadAfter - the plugins which should be loaded before this plugin
      minUltiToolsVersion - the minimum version of UltiTools required by this plugin
      mainClass - the main class of the plugin
      resourceFolderPath - the path to the resource folder
  • Method Details

    • initializeLanguage

      private Language initializeLanguage()
      Initializes the language object
      Returns:
      Language object
    • resolveLanguageCode

      private String resolveLanguageCode()
      Resolves which language code to actually load, consulting Localized.supported() before createLanguageFromPath(String) picks a file (D-20/D-21/WIRE-10). Prefers the configured code; if it is absent from a non-empty supported(), prefers "en" when supported() contains it, otherwise the first entry in supported()'s iteration order. An empty supported() is "no information" - the configured code is returned unchanged and nothing is logged.
      Returns:
      the language code to actually load
    • createLanguageFromPath

      private Language createLanguageFromPath(String folderPath)
      Creates a Language object from the given resource folder path
      Parameters:
      folderPath - the resource folder path
      Returns:
      Language object
    • resolveDiskLanguage

      private Language resolveDiskLanguage(String folderPath, String code)
      Resolves the on-disk language catalogue for code, trying each of LANGUAGE_EXTENSIONS in order and returning the first that exists -- independently of whichever extension resolveJarLanguage(String) resolves for the same code (13-REVIEW round 5, own deep review of 0ddc95f, finding 1).
      Returns:
      the resolved on-disk language, or null if none of the extensions exist on disk
    • resolveJarLanguage

      private Language resolveJarLanguage(String code)
      Resolves the jar-bundled language catalogue for code, trying each of LANGUAGE_EXTENSIONS in order and returning the first that exists -- independently of whichever extension resolveDiskLanguage(String, String) resolves for the same code (13-REVIEW round 5, own deep review of 0ddc95f, finding 1).
      Returns:
      the resolved jar-bundled language, or null if none of the extensions exist in the jar
    • loadLanguageFromDisk

      private Language loadLanguageFromDisk(String folderPath, String code, String extension)
      Reads <folderPath>/lang/<code><extension> if it exists, else null.

      D-05/D-06/D-07 (#441): before returning, applies the recorded-provenance decision -- an upgraded server whose language file was never touched since extraction gets it silently replaced by the current jar's copy; one an operator customised is left alone, with a per-key warning for any key whose placeholder count moved out from under it. Layered in front of the resolution split, not inside Language: resolveLanguageWithProvenance(java.lang.String, java.io.File, java.lang.String, java.lang.String) decides which bytes/dictionary this method returns, and Language.withFallback(com.ultikits.ultitools.entities.Language) (unchanged) still owns filling a key this dictionary lacks entirely.

      16-05 / CodeQL java/zipslip alert #11 (CWE-22): code is only lightly restricted by Localized.languageCodeOf(String)'s character allowlist when it comes from a scanned jar/directory entry -- and not restricted at all when it comes from a hostile config.yml language: value, since resolveLanguageCode() returns an unmatched configured code UNCHANGED whenever Localized.supported() is empty. This is therefore the last line of defence: before this file is read (or, deeper in resolveLanguageWithProvenance(java.lang.String, java.io.File, java.lang.String, java.lang.String), overwritten), its canonical path must stay inside <folderPath>/lang's own canonical path -- mirroring the guard saveResources() already applies to extracted jar entries. A violation degrades to the same null (“not loadable”) outcome as a missing file, never a thrown exception.

    • isWithinDirectory

      private static boolean isWithinDirectory(File baseDir, File candidate)
      Verifies that candidate's canonical path is contained within baseDir's own canonical path -- the same Zip Slip guard saveResources() already applies to extracted jar entries, generalized here for every other place an untrusted language code is turned into a File (16-05 / CodeQL java/zipslip alert #11, CWE-22). Neither baseDir nor candidate needs to exist: File.getCanonicalPath() is defined for a non-existent path too, normalizing .. segments lexically wherever the real filesystem does not need to be consulted (the same property saveResources() already relies on for a file it is about to create).

      Returns false (never throws) on any I/O failure resolving either canonical path, since an uncanonicalizable path is not a basis for trusting containment either.

      Parameters:
      baseDir - the directory candidate must resolve inside
      candidate - the file built from an untrusted language code
      Returns:
      whether candidate's canonical path is baseDir's canonical path or a descendant of it
    • resolveLanguageWithProvenance

      private Language resolveLanguageWithProvenance(String folderPath, File file, String resourcePath, String extension)
      Applies the D-05/D-06 recorded-provenance decision to a single on-disk language file. Four branches, mutually exclusive:
      1. Recorded hash present and equal to the disk file's current hash -- never touched by the operator since extraction: overwrite the disk file with the current jar's bytes, re-record the new (jar) hash as the baseline, and log one informative line.
      2. Recorded hash present and different from the disk file's current hash: this by ITSELF is NOT evidence of operator customisation -- see the branch-2 code comment below (Codex finding, thread PRRT_kwDOIcF9Es6i2kao, P2) for why, and for the content-comparison this branch now performs before concluding either way.
      3. No recorded hash, but the disk bytes already equal the jar's -- provably unmodified, unknown provenance only because an older jar (pre-#441) extracted it: record the hash as the new baseline and enter the normal mechanism, with no overwrite this pass (D-06). Never adopt an unequal disk hash as a baseline -- that would silently overwrite a real customisation on the next jar change.
      4. No recorded hash and the disk bytes differ from the jar's -- unknown provenance, assume customisation: never record, never overwrite; the per-key placeholder-arity override still applies.
      A jar entry absent for this exact resourcePath (D-05's stated exception) short- circuits before any of the four branches: the disk file is left alone and nothing is recorded, since there is nothing to compare against.

      Every branch above that concludes "customisation" derives that conclusion from an ACTUAL disk-bytes-vs-jar-bytes comparison, never from the record alone -- branches 3 and 4 already did (their own classification IS a content comparison); branch 2 was corrected to do the same (see its own code comment). The record is provenance BOOKKEEPING, not the source of truth for whether a file has been customised; it can go stale (most deterministically, since the previous round, when the sidecar itself is symlinked or read-only and its own write is refused) without that staleness ever being able to cause a PERMANENT misclassification.

      Parameters:
      folderPath - the module's on-disk resource folder root
      file - the on-disk language file, already confirmed to exist
      resourcePath - the extracted resource's path relative to the resource folder (e.g. "lang/en.json"), matching saveResources()'s own keys
      extension - the language file extension (".json", ".yml" or ".yaml")
      Returns:
      the language that loadLanguageFromDisk(java.lang.String, java.lang.String, java.lang.String) should treat as "the disk language"
    • resolveNeverTouchedSinceExtraction

      private Language resolveNeverTouchedSinceExtraction(File file, byte[] jarBytes, String jarHash, String diskHash, File resourceFolder, String resourcePath, String extension)
      Branch 1 of resolveLanguageWithProvenance(java.lang.String, java.io.File, java.lang.String, java.lang.String): the recorded hash equals the disk file's current hash -- the operator has never touched this file since it was extracted, so it is safe to overwrite from the current jar. Extracted into its own method (PMD NPathComplexity: once branch 2 in the caller gained its own content-comparison check for the Codex finding on UltiToolsPlugin.java:515, thread PRRT_kwDOIcF9Es6i2kao, this branch's own nested writeBytes/try/catch shape pushed the combined method over Codacy's threshold; splitting it out is a structural change only, the behaviour below is unchanged from before that finding).

      Codex round 1, P2: skip the write entirely when the bundled content has not actually changed since it was last synced -- the common case on every restart after the first successful sync, not an edge case. Rewriting identical bytes and logging "has been updated" every single time is misleading, touches the file's mtime for no reason, and fails needlessly on an installation that hardens module resources read-only after provisioning.

      Only records the new baseline and logs success once the write is CONFIRMED to have landed -- re-hashes the bytes actually on disk afterward (mirroring saveResources()'s own pattern) rather than trusting the precomputed jarHash, so a partial/failed write can never be misreported as success (WR-01). Codex round 2, P2: record(...) swallows its own IOException and returns void, so a caller cannot otherwise tell a sidecar write failure from success -- the record is read back to confirm it actually persisted before claiming success in the log; logging "has been updated" when the file WAS refreshed but the sidecar was NOT would misrepresent provenance tracking as healthy. Deliberately not reverted on failure: a second write introduces its own atomicity risk for a genuinely rare failure, and the next boot's hash mismatch safely falls into branch 2's own content-comparison fix instead, rather than a second write attempt here.

      Codex round on UltiToolsPlugin.java:498, P2: this used to be an unconditional readLanguageFile(...), skipping the per-key placeholder-arity guard on EXACTLY the paths where a stale value is most likely -- writeBytes(file, jarBytes) returning false (read-only file, symlink, unpreservable ownership, or an I/O failure mid-write) leaves the OLD disk bytes in place while jarBytes has already moved on, so a key whose bundled arity shrank (two placeholders to one) can still carry the old, wider disk value. A stale disk value with MORE placeholders than the call site now supplies throws MissingFormatArgumentException at format time -- extra bundled arguments over a narrower stale value are silently ignored by String.format(java.lang.String, java.lang.Object...), not an error. Applying the override UNCONDITIONALLY -- not only on the writeBytes-failed path -- means the write's own outcome stops mattering for this guarantee: when the write succeeded, disk and jar bytes are now identical, so every key's arity trivially matches and this is a no-op past the extra parse; when it failed, this is precisely the protection this method exists to provide.

      Parameters:
      file - the on-disk language file, already confirmed to exist
      jarBytes - the current bundled bytes for this resource, already confirmed non-null by the caller
      jarHash - jarBytes's own SHA-256 digest, already computed by the caller
      diskHash - file's current SHA-256 digest, already computed by the caller -- equal to the recorded provenance hash, which is what routed the caller into this method in the first place
      resourceFolder - the module's resource folder root (the sidecar's own location)
      resourcePath - the extracted resource's path relative to the resource folder
      extension - the language file extension (".json", ".yml" or ".yaml")
      Returns:
      the resolved language for this file
    • readLanguageFile

      private Language readLanguageFile(File file, String extension)
      Reads an on-disk language file exactly as the pre-#441 loadLanguageFromDisk did -- extracted unchanged so both provenance branches that keep the disk file's own parse (branches 1 and 3 in resolveLanguageWithProvenance(java.lang.String, java.io.File, java.lang.String, java.lang.String)) share the same reading logic the jar-absent short-circuit also uses.
    • applyPlaceholderArityOverride

      private Language applyPlaceholderArityOverride(File file, byte[] jarBytes, String extension, String resourcePath)
      Builds the disk language for the "operator customisation" branches (2 and 4 in resolveLanguageWithProvenance(java.lang.String, java.io.File, java.lang.String, java.lang.String)): the disk dictionary stays authoritative for every key, except a key the jar's value proves has LOST a placeholder relative to the disk value -- that key is overridden to the jar's value and warned about once, naming the module, the file and the key but never either value (T-16-04-02). A key present only in the jar is deliberately left out of the returned dictionary: Language.withFallback(com.ultikits.ultitools.entities.Language) (unchanged) already resolves a key the disk dictionary lacks entirely, and duplicating that here would just be a second, redundant path to the same answer.

      Two independent detectors decide "lost a placeholder", evaluated SEPARATELY rather than through one shared code path (#524):

      • placeholderArity(java.lang.String) for %s/%d -- SYMMETRIC equality of the highest required String.format argument position. Either side moving, in either direction, is a mismatch: both sides use the same dialect, so any count change at all means the parameter shape changed.
      • missingBracePlaceholder(java.lang.String, java.lang.String) for {0}/{PLAYER}-style tokens -- ONE-DIRECTIONAL presence, not a count. It fires only when a token the JAR value contains is ABSENT from the disk value; a disk value that keeps every bundled token but adds a token of its own, or rewords everything around them, never fires.
      The two cannot share one code path: collapsing them into a single "count differs" rule would make the brace detector symmetric too, and a symmetric brace rule fires on an operator's own customisation the moment its OWN brace count doesn't happen to match the jar's -- exactly the false-positive class PLACEHOLDER_PATTERN's own javadoc already documents Codex round 6 eliminating for the % dialect (a permissive pattern reading ordinary text as a placeholder). One-directional token presence is the brace dialect's equivalent fix: it flags a slot that disappeared under the operator and nothing else -- never an operator who added something of their own.

      What an operator loses in the one case this DOES fire: nothing structural -- their file is never touched, never rewritten. That one message renders with the bundled wording (not their customised wording) until they notice the warning and re-add the missing token themselves.

    • placeholderArity

      private static int placeholderArity(String value)
      Returns the highest String.format argument POSITION value requires -- not the count of distinct positions used (Codex round 2, P2). "%2$s" alone requires an args array of length (at least) 2, since String.format demands every position up to the highest one referenced be present, even if a lower position (here, 1) is never itself rendered -- so its arity is 2, not 1. Counting DISTINCT positions (a one-element set for both "%2$s" alone and "%s" alone) would wrongly call those two equal.

      A value repeating one EXPLICIT index twice (e.g. "%1$s and %1$s again") still has arity one, since both conversions consume the SAME argument and neither raises the highest-position watermark past 1; a naive occurrence count would call it two and warn on a rewording that changed nothing about the message's parameter shape. An unindexed conversion (this framework's own catalogues use only this form) consumes the NEXT sequential position, so two unindexed %s conversions in one value require arity two. %% (a literal percent) consumes no argument and is excluded -- PLACEHOLDER_PATTERN itself (Codex round 6, P2) no longer matches anything outside %s/%d/%% at all, so %n and every other Formatter conversion this framework never emits cannot reach this method to begin with.

      Parameters:
      value - a language value, or null
      Returns:
      the highest argument position value's String.format conversions require, or 0 for null or a value with none
    • missingBracePlaceholder

      private static boolean missingBracePlaceholder(String diskValue, String jarValue)
      Reports whether jarValue contains a brace-style placeholder token (see BRACE_TOKEN_PATTERN) that diskValue does not -- ONE-DIRECTIONALLY, and by TOKEN PRESENCE, not by count (#524). See applyPlaceholderArityOverride(java.io.File, byte[], java.lang.String, java.lang.String)'s own javadoc for why this is deliberately asymmetric, unlike placeholderArity(java.lang.String)'s symmetric check: an operator's value that keeps every bundled token -- however reworded, recoloured, or however many of its OWN extra tokens it adds -- must never be flagged. Only a token that existed in the bundled value and is genuinely gone from the operator's value counts as a loss.

      A jar value with no brace tokens at all can never report a loss (there is nothing to lose), independent of whatever the disk value happens to contain -- this is what keeps a module's %s/%d-only keys, and any decorative brace an operator adds on their own, outside this check entirely.

      Parameters:
      diskValue - the operator's on-disk value for this key
      jarValue - the current bundled value for the same key
      Returns:
      true if jarValue has at least one brace token absent from diskValue
    • bracePlaceholderTokens

      private static Set<String> bracePlaceholderTokens(String value)
      Returns the distinct brace-style placeholder tokens (see BRACE_TOKEN_PATTERN) found in value -- e.g. {"0", "PLAYER"} for "Hi {PLAYER}, you are #{0}". Iteration order is insertion order and otherwise unused: the only caller (missingBracePlaceholder(java.lang.String, java.lang.String)) only ever calls containsAll on the result.
      Parameters:
      value - a language value, or null
      Returns:
      the distinct token names found, or an empty set for null or a value with none
    • readFlatDictionary

      private static Map<String,String> readFlatDictionary(File file, String extension)
      Reads a flat key -> value dictionary from a language file on disk, for the placeholder-arity comparison only -- Language itself exposes no way to enumerate its keys, so this is a small, independent read of the same file formats, not a change to Language's own parsing. Degrades to an empty map on any read/parse failure (an empty file is "no keys", never an error, per D-05).
    • readFlatDictionary

      private static Map<String,String> readFlatDictionary(byte[] bytes, String extension)
      Reads a flat key -> value dictionary from jar-bundled bytes, mirroring the file overload above for the jar side of the comparison.
    • readFlatDictionary

      private static Map<String,String> readFlatDictionary(Reader reader, String extension)
    • writeBytes

      private boolean writeBytes(File file, byte[] bytes)
      Writes bytes to file, returning whether the write actually succeeded (WR-01) -- the caller must not record a new provenance baseline or log a success line for a write that threw partway through.

      Round following PosixAttributePreserver.java:104 (symlink finding on ResourceHashSidecar.java:320, thread PRRT_kwDOIcF9Es6i2I49, P2): this method's entire operation -- the operator-pinned-read-only refusal, the symlink refusal, the temp-file-then-atomic-move mechanism, and the POSIX attribute preservation -- now lives in PosixAttributePreserver.replaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener), shared with ResourceHashSidecar#writeAll's identical contract, so the two write paths cannot disagree on any of these properties again the way they disagreed on attribute preservation (round 3), an unreadable-attributes edge case (round 6), and symlink handling (this round) one at a time. This method is now a thin PosixAttributePreserver.ReplaceInPlaceListener supplying this class's own getLogger()/getPluginName() warning text; the full behavioural contract (what each refusal means, why each is conservative rather than best-effort) is documented on PosixAttributePreserver.replaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener) and PosixAttributePreserver.ReplaceInPlaceListener themselves, not duplicated here.

      The symlink refusal's exact semantics, unchanged from Codex round 11, P2: a symlinked language file (an operator-managed shared-translations layout, e.g. lang/en.json pointing at a shared store) is left completely untouched -- REFUSE, not write-through to the link's target. See PosixAttributePreserver.ReplaceInPlaceListener.onSymbolicLink().

    • readEmbeddedResourceBytes

      private byte[] readEmbeddedResourceBytes(String resourcePath)
      Reads the raw bytes of resourcePath (e.g. "lang/en.json") from this module's own CodeSource location, or null if it is not present there. Mirrors loadLanguageFromJar(String, String)'s directory/jar dual branch exactly (13-REVIEW CR-01, issue #412 follow-up) but returns raw bytes instead of a parsed Language, so the D-05/D-06 provenance decision can hash and, in the overwrite branch, write those exact bytes to disk.
    • readEmbeddedResourceBytesFromDirectory

      private byte[] readEmbeddedResourceBytesFromDirectory(File location, String resourcePath)
      The exploded-classpath (directory CodeSource) branch of readEmbeddedResourceBytes(String), split out to keep that method's NPath complexity under the Codacy/PMD threshold once the 16-05 / CodeQL java/zipslip alert #11 containment guard was added -- purely a structural split, the guard's own behaviour is unchanged.
      Parameters:
      location - the directory CodeSource location
      resourcePath - the resource path to read, e.g. "lang/en.json"
      Returns:
      the resource's raw bytes, or null if absent, escaping, or unreadable
    • loadLanguageFromJar

      private Language loadLanguageFromJar(String code, String extension)
      Reads lang/<code><extension> from the module's own CodeSource location if present, else null. Mirrors Localized.scanLangResources(URL)'s directory/jar branch (13-REVIEW CR-01, issue #412 follow-up): an exploded classpath (dev workspace, IDE launch, or a module test that instantiates a real UltiToolsPlugin subclass) has a directory as its CodeSource, and Localized.supported() already scans that directory directly -- this method must not disagree by unconditionally trying (and failing) to open the directory as a JarFile first.

      The resource path is built with '/' for the jar-entry lookup and File.separator for the on-disk lookup: jar entry names always use a forward slash, so the separator form would silently find nothing on a Windows host, and the reverse holds for a real file path.

    • resolveCodeSourceFile

      private static File resolveCodeSourceFile(URL location)
      Resolves a CodeSource location as a File, decoding any percent-escaped characters (spaces, non-ASCII, etc.) that URL.getPath() does not decode on its own (13-REVIEW WR-03) -- passing an undecoded %20... path straight to File(String) or JarFile(File) finds nothing when the module is installed under a path containing a space or other URI-escaped character. Falls back to the previous raw-path substring logic -- matching getInputStream()'s own try/catch(URISyntaxException) shape for the same CodeSource location -- for the rare case the location cannot be expressed as a URI at all.
      Parameters:
      location - the CodeSource.getLocation() URL, never null
      Returns:
      the resolved location as a File
    • parseLanguageResource

      private static Language parseLanguageResource(BufferedReader reader, String extension) throws IOException
      Parses a lang/* resource already opened as a BufferedReader -- shared by both the on-disk (exploded directory) and in-jar branches of loadLanguageFromJar(String, String) so the two stay in sync.
      Throws:
      IOException
    • loadPluginConfiguration

      private org.bukkit.configuration.file.YamlConfiguration loadPluginConfiguration()
      Loads the plugin configuration from plugin.yml
      Returns:
      YamlConfiguration object with default values if loading fails
    • setContext

      @Internal public void setContext(SimpleContainer context)
      Injects the IoC container created for this plugin.

      Called by PluginManager while a module is being loaded, before registerSelf() runs. It is not part of the module-facing API — a module that calls it replaces the container the framework already wired up, losing every bean that was injected into it.

      Deliberately still public: PluginManager lives in another package, so this cannot be narrowed to package-private, and deleting it outright would remove a public method, which the compatibility policy forbids in a PATCH release. The annotation is a signal to humans and IDEs; it enforces nothing at runtime.

      Parameters:
      context - the container created for this plugin
    • setDataScope

      @Internal public void setDataScope(DataScope scope)
      Injects the DataScope credential PluginManager minted for this module (D-17). Called by PluginManager right after minting, before wireAop runs -- the same lifecycle point setContext(com.ultikits.ultitools.context.SimpleContainer) documents.

      Deliberately still public, for the same reason setContext(com.ultikits.ultitools.context.SimpleContainer) is: PluginManager lives in another package and cannot be narrowed to package-private, and the compatibility policy forbids removing a public method in a PATCH release. The annotation is a signal to humans and IDEs; it enforces nothing at runtime.

      Parameters:
      scope - the scope minted for this plugin
      Since:
      6.3.0
    • getConfigManager

      public static ConfigManager getConfigManager()
      Returns:
      the config manager
    • getListenerManager

      public static ListenerManager getListenerManager()
      Returns:
      the listener manager
    • getCommandManager

      public static CommandManager getCommandManager()
      Returns:
      the command manager
    • getPluginManager

      public static PluginManager getPluginManager()
      Returns:
      the plugin manager
    • initConfig

      private void initConfig() throws IOException
      Initializes the configuration entity.
      Throws:
      IOException
    • diffGetAllConfigsOverride

      private void diffGetAllConfigsOverride()
      Diffs a Configurable.getAllConfigs() override against what auto-registration already registered for this module (D-06 / SILENT-18 / #336), called once right after package-scan auto-registration finishes.

      An empty override (the interface default - the module never wrote Configurable.getAllConfigs()) has nothing to compare and nothing to log. A non-empty override whose every configFilePath was already registered by the package scan is pure redundancy, logged at Level.FINE only. A non-empty override naming a configFilePath the scan never registered is real capability loss - #336's warning-only ask cannot tell these two cases apart, so this refuses the module and names every missing entity instead of guessing.

      Throws:
      ConfigurationException - if the override names a configFilePath auto-registration never registered
    • getInputStream

      private InputStream getInputStream() throws IOException
      Throws:
      IOException
    • resolveJarFileNameForError

      private String resolveJarFileNameForError()
      Best-effort resolution of this module's own JAR file name, for the name:-missing refusal message only. Never throws -- falls back to the class name if the code source is unavailable (e.g. when running from unpacked classes in a test).
      Returns:
      the JAR file name, or this class's name if it cannot be determined
    • getConfigFolder

      protected final String getConfigFolder()
    • getConfigFile

      protected final File getConfigFile(String path)
    • getConfig

      public <T extends AbstractConfigEntity> T getConfig(Class<T> configType)
      Description copied from interface: Configurable
      Get config by config type.
      Specified by:
      getConfig in interface Configurable
      Type Parameters:
      T - Config type
      Parameters:
      configType - Config type
      Returns:
      Config
    • getConfig

      public <T extends AbstractConfigEntity> T getConfig(String path, Class<T> configType)
      Description copied from interface: Configurable
      Get config by config path and config type.
      Specified by:
      getConfig in interface Configurable
      Type Parameters:
      T - Config type
      Parameters:
      path - Config path
      configType - Config type
      Returns:
      Config
    • getConfigs

      public <T extends AbstractConfigEntity> List<T> getConfigs(Class<T> configType)
    • saveConfig

      public <T extends AbstractConfigEntity> void saveConfig(String path, Class<T> configType) throws IOException
      Description copied from interface: Configurable
      Save config by config type.
      Specified by:
      saveConfig in interface Configurable
      Type Parameters:
      T - Config type
      Parameters:
      path - Config path
      configType - Config type
      Throws:
      IOException - IOException
    • saveResources

      private void saveResources()
      Extracts this module's embedded res/lang/config jar resources into resourceFolderPath. Guarantees canonical-path validation before every write: each extracted entry's resolved destination is checked against the resource folder's own canonical path, and any entry whose path would resolve outside it (a Zip Slip attempt) is skipped with a warning rather than written.

      Every file this method actually extracts gets its provenance recorded, in one batch via ResourceHashSidecar.recordAll(File, Map) after the whole extraction pass completes (Codex round 4, P2) -- across all three prefixes, D-07 -- so a later boot can tell "the operator edited this" from "an old jar extracted this and nobody has touched it since" (#441, D-05/D-06). A file this method skips (already present on disk) gets no record here: the skip already means the file predates this mechanism, or was already decided on by loadLanguageFromDisk(java.lang.String, java.lang.String, java.lang.String) on a previous boot.

    • getDataOperator

      public final <T extends BaseDataEntity<String>> DataOperator<T> getDataOperator(Class<T> dataClazz)
      Gets data operator. Refuses outright if dataClazz is not registered to this module (D-14) -- checked against the same DataScope minted for this module at load, via the same refusal DataStore.getOperator(DataScope, Class) builds, so the exception type, error code, and message shape are identical regardless of which entry point a caller reaches.

      02-13 (CR-03): before this, dataScope.owns(...) was checked here inline and this method then delegated to the deprecated getOperator(UltiToolsPlugin, Class) overload directly -- so DataStore.getOperator(DataScope, Class), the method D-17/02-07 built specifically as the credential-typed supported path, had zero real callers anywhere in the framework. This now routes through it, so production actually uses the path it was built for. dataScope is normally non-null by the time any module calls this (set by PluginManager right after minting, before registerSelf() runs); the null fallback below only covers a bare instance constructed outside the normal PluginManager load flow (e.g. a test), where the deprecated overload's own checkOwnership(...) call still refuses correctly on its own.

      Type Parameters:
      T - the type of the data entity
      Parameters:
      dataClazz - the class of the data entity
      Returns:
      the data operator
      Throws:
      DataAccessException - if dataClazz is not registered to this module
    • getLanguageCode

      public final String getLanguageCode()
      Returns:
      language code
    • getLanguage

      public final Language getLanguage()
      Returns:
      the language
    • i18n

      public String i18n(String str)
      Parameters:
      str - the string to be localized
      Returns:
      the localized string
    • i18n

      public final String i18n(String code, String str)
      Description copied from interface: Localized
      Returns a localized string for the given default text. The code parameter is not honoured by the framework's own implementation -- UltiToolsPlugin overrides this method final and resolves directly against the module's already-loaded Language, discarding code entirely. Language selection happens once at load time, via Localized.supported(), not per call. str is returned unchanged when it is absent as a key from that already-loaded dictionary.
      Specified by:
      i18n in interface Localized
      Parameters:
      code - language code -- not honoured by the framework's own i18n(String, String) override
      str - default display text, also the dictionary lookup key
      Returns:
      a localized string, or str unchanged if the key is absent from the loaded dictionary
    • isNewerVersionThan

      public boolean isNewerVersionThan(UltiToolsPlugin plugin)
      Parameters:
      plugin - the plugin to be checked
      Returns:
      whether the plugin is newer than the given plugin
    • onUnregister

      protected void onUnregister()
      Extension point for a module's own unload cleanup.

      Called by unregisterSelf() before the framework unregisters this plugin's commands and listeners (D-02), so the module's own beans are still alive while this runs. unregisterSelf() is final and always calls this hook and the framework's own unregistration afterward -- a module cannot skip either by overriding unregisterSelf() itself, because that is no longer possible (D-01). The default body does nothing; override this method, not unregisterSelf(), to add cleanup work.

      If this hook throws, unregisterSelf() still unregisters this module's own commands and listeners before the exception propagates (WR-01, 16-REVIEW-lifecycle.md) -- a throwing hook is surfaced to the caller, not swallowed, but it cannot skip the framework's own cleanup the way an unguarded super call could. If a cleanup step also fails, that failure does not replace this hook's own exception: it is attached to it via Throwable.addSuppressed(java.lang.Throwable) instead, so the module author's own failure is always the one that propagates and is never silently lost behind a secondary framework failure (Codex review on #457, issue #484).

    • unregisterSelf

      public final void unregisterSelf()
      Description copied from interface: IPlugin
      UltiTools will call this method when unregistering this plugin module, similar to JavaPlugin.onDisable()
      Specified by:
      unregisterSelf in interface IPlugin
    • runUnregisterStep

      private static Throwable runUnregisterStep(Throwable previousFailure, Runnable step)
      Runs one unregisterSelf() cleanup step, collecting any failure onto previousFailure instead of letting it replace or be replaced by another step's failure.

      step can only throw an unchecked exception -- onUnregister(), CommandManager#unregisterAll, and ListenerManager#unregisterAll all declare no checked exceptions -- so catching RuntimeException | Error here covers every real case without the width of catching Throwable (this is a deliberate cleanup barrier, not a broad catch: unregisterSelf() always runs every remaining step and always rethrows exactly one collected failure, per its own javadoc).

      Parameters:
      previousFailure - the failure already collected from an earlier step, or null if every earlier step (if any) succeeded
      step - the cleanup step to run
      Returns:
      previousFailure, with step's own failure (if any) attached to it via Throwable.addSuppressed(java.lang.Throwable); or, if previousFailure was null, step's own failure, or null if step completed normally
    • onReload

      protected void onReload()
      Extension point for a module's own reload work.

      Called by reloadSelf() after the framework's own reload steps -- config reload, config-bound timing refresh (#531), language-catalogue refresh, @ConditionalOnConfig drift report, and the framework-owned per-module reload log line (D-02, D-03) -- so a real-work override sees the already-reloaded configuration rather than the stale one. reloadSelf() is final and always runs its own steps first; a module cannot skip them by overriding reloadSelf() itself, because that is no longer possible (D-01). The default body does nothing; override this method, not reloadSelf(), to add reload work.

    • reloadSelf

      public final void reloadSelf()
      Reload this plugin's configuration and language files.

      Also reports (but does not act on) any @ConditionalOnConfig drift: the condition is evaluated once, at component-scan time during startup, so a reload can only log that a watched key has changed direction since then -- it never registers, unregisters, or rebuilds anything (issue #392, D-01). Right after the configuration reload, and only if it succeeded, the module's config-bound @Scheduled tasks and @CmdCD cooldowns pick up their reloaded values (#531; see PluginManager#applyReloadedConfigBindings). final and always runs its own steps, then logs one framework-owned INFO line naming this module (D-03), then calls onReload() -- a module can no longer skip any of this by overriding reloadSelf() itself, because that override point no longer exists (D-01).

      Specified by:
      reloadSelf in interface IPlugin
    • getLogger

      public PluginLogger getLogger()
      Returns:
      plugin logger