Class UltiToolsPlugin
- All Implemented Interfaces:
Configurable,IPlugin,Localized
- Version:
- 1.0.0
- Author:
- wisdomme
-
Field Summary
FieldsModifier and TypeFieldDescriptionprivate static final com.google.gson.GsonA private, independent JSON reader for the D-05 placeholder-arity comparison only -- not a change toLanguage's own Gson usage, which stays entirely insideLanguage.java(untouched by this plan).private static final Typeprivate static final PatternMatches a brace-style placeholder token as used by the MODULElangcatalogues this mechanism protects --{0}/{1}positional or{PLAYER}/{WORLD}named, substituted with plainString#replace, neverjava.util.Formatter.private SimpleContainerprivate DataScopeprivate final Stringprivate Languageprivate static final String[]Language file extensions, in the order they are tried.private static final LoggerNamed logger for the D-03 per-module reload line -- seeRELOAD_LOG_MESSAGE_KEY.private final Stringprivate final intprivate static final PatternMatches ajava.util.Formatterconversion specifier, e.g.private final String(package private) static final StringFramework i18n key (this class's ownlang/en.json/lang/zh.jsoncatalogue, not a module's) for the per-module reload linereloadSelf()logs after its three steps (D-03).private Stringprivate final String -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionprivate LanguageapplyPlaceholderArityOverride(File file, byte[] jarBytes, String extension, String resourcePath) Builds the disk language for the "operator customisation" branches (2 and 4 inresolveLanguageWithProvenance(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).bracePlaceholderTokens(String value) Returns the distinct brace-style placeholder tokens (seeBRACE_TOKEN_PATTERN) found invalue-- e.g.private LanguagecreateLanguageFromPath(String folderPath) Creates a Language object from the given resource folder pathprivate voidDiffs aConfigurable.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.static CommandManager<T extends AbstractConfigEntity>
TGet config by config type.<T extends AbstractConfigEntity>
TGet config by config path and config type.protected final FilegetConfigFile(String path) protected final Stringstatic ConfigManager<T extends AbstractConfigEntity>
List<T> getConfigs(Class<T> configType) final <T extends BaseDataEntity<String>>
DataOperator<T> getDataOperator(Class<T> dataClazz) Gets data operator.private InputStreamfinal Languagefinal Stringstatic ListenerManagerstatic PluginManagerfinal StringReturns a localized string for the given default text.private voidInitializes the configuration entity.private LanguageInitializes the language objectbooleanisNewerVersionThan(UltiToolsPlugin plugin) private static booleanisWithinDirectory(File baseDir, File candidate) Verifies thatcandidate's canonical path is contained withinbaseDir's own canonical path -- the same Zip Slip guardsaveResources()already applies to extracted jar entries, generalized here for every other place an untrusted language code is turned into aFile(16-05 / CodeQLjava/zipslipalert #11, CWE-22).private LanguageloadLanguageFromDisk(String folderPath, String code, String extension) Reads<folderPath>/lang/<code><extension>if it exists, elsenull.private LanguageloadLanguageFromJar(String code, String extension) private org.bukkit.configuration.file.YamlConfigurationLoads the plugin configuration from plugin.ymlprivate static booleanmissingBracePlaceholder(String diskValue, String jarValue) Reports whetherjarValuecontains a brace-style placeholder token (seeBRACE_TOKEN_PATTERN) thatdiskValuedoes not -- ONE-DIRECTIONALLY, and by TOKEN PRESENCE, not by count (#524).protected voidonReload()Extension point for a module's own reload work.protected voidExtension point for a module's own unload cleanup.private static LanguageparseLanguageResource(BufferedReader reader, String extension) Parses alang/*resource already opened as aBufferedReader-- shared by both the on-disk (exploded directory) and in-jar branches ofloadLanguageFromJar(String, String)so the two stay in sync.private static intplaceholderArity(String value) Returns the highestString.formatargument POSITIONvaluerequires -- not the count of distinct positions used (Codex round 2, P2).private byte[]readEmbeddedResourceBytes(String resourcePath) Reads the raw bytes ofresourcePath(e.g.private byte[]readEmbeddedResourceBytesFromDirectory(File location, String resourcePath) The exploded-classpath (directoryCodeSource) branch ofreadEmbeddedResourceBytes(String), split out to keep that method's NPath complexity under the Codacy/PMD threshold once the 16-05 / CodeQLjava/zipslipalert #11 containment guard was added -- purely a structural split, the guard's own behaviour is unchanged.readFlatDictionary(byte[] bytes, String extension) Reads a flatkey -> valuedictionary from jar-bundled bytes, mirroring the file overload above for the jar side of the comparison.readFlatDictionary(File file, String extension) readFlatDictionary(Reader reader, String extension) private LanguagereadLanguageFile(File file, String extension) Reads an on-disk language file exactly as the pre-#441loadLanguageFromDiskdid -- extracted unchanged so both provenance branches that keep the disk file's own parse (branches 1 and 3 inresolveLanguageWithProvenance(java.lang.String, java.io.File, java.lang.String, java.lang.String)) share the same reading logic the jar-absent short-circuit also uses.final voidReload this plugin's configuration and language files.private static FileresolveCodeSourceFile(URL location) Resolves aCodeSourcelocation as aFile, decoding any percent-escaped characters (spaces, non-ASCII, etc.) thatURL.getPath()does not decode on its own (13-REVIEW WR-03) -- passing an undecoded%20...path straight toFile(String)orJarFile(File)finds nothing when the module is installed under a path containing a space or other URI-escaped character.private LanguageresolveDiskLanguage(String folderPath, String code) Resolves the on-disk language catalogue forcode, trying each ofLANGUAGE_EXTENSIONSin order and returning the first that exists -- independently of whichever extensionresolveJarLanguage(String)resolves for the same code (13-REVIEW round 5, own deep review of0ddc95f, finding 1).private StringBest-effort resolution of this module's own JAR file name, for thename:-missing refusal message only.private LanguageresolveJarLanguage(String code) Resolves the jar-bundled language catalogue forcode, trying each ofLANGUAGE_EXTENSIONSin order and returning the first that exists -- independently of whichever extensionresolveDiskLanguage(String, String)resolves for the same code (13-REVIEW round 5, own deep review of0ddc95f, finding 1).private StringResolves which language code to actually load, consultingLocalized.supported()beforecreateLanguageFromPath(String)picks a file (D-20/D-21/WIRE-10).private LanguageresolveLanguageWithProvenance(String folderPath, File file, String resourcePath, String extension) Applies the D-05/D-06 recorded-provenance decision to a single on-disk language file.private LanguageresolveNeverTouchedSinceExtraction(File file, byte[] jarBytes, String jarHash, String diskHash, File resourceFolder, String resourcePath, String extension) Branch 1 ofresolveLanguageWithProvenance(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.private static ThrowablerunUnregisterStep(Throwable previousFailure, Runnable step) Runs oneunregisterSelf()cleanup step, collecting any failure ontopreviousFailureinstead of letting it replace or be replaced by another step's failure.<T extends AbstractConfigEntity>
voidsaveConfig(String path, Class<T> configType) Save config by config type.private voidvoidsetContext(SimpleContainer context) Injects the IoC container created for this plugin.voidsetDataScope(DataScope scope) Injects theDataScopecredentialPluginManagerminted for this module (D-17).final voidUltiTools will call this method when unregistering this plugin module, similar toJavaPlugin.onDisable()private booleanwriteBytes(File file, byte[] bytes) Writesbytestofile, 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.Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, waitMethods inherited from interface com.ultikits.ultitools.interfaces.Configurable
getAllConfigsMethods inherited from interface com.ultikits.ultitools.interfaces.IPlugin
registerSelf
-
Field Details
-
LANGUAGE_EXTENSIONS
Language file extensions, in the order they are tried..jsonstays first so a module shipping both keeps exactly the behaviour it had before 6.3.0..ymland.yamlwere added for #389: eight modules ship YAML, and the loader silently produced an empty dictionary for every one of them. -
LOGGER
Named logger for the D-03 per-module reload line -- seeRELOAD_LOG_MESSAGE_KEY. -
RELOAD_LOG_MESSAGE_KEY
Framework i18n key (this class's ownlang/en.json/lang/zh.jsoncatalogue, not a module's) for the per-module reload linereloadSelf()logs after its three steps (D-03). Package-private soUltiToolsPluginLifecycleHookTestcan 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
Matches ajava.util.Formatterconversion specifier, e.g.%sin"Hello, %s!", or%1$sfor an explicit argument index. Used only byplaceholderArity(String), one of the two placeholder-loss detectorsapplyPlaceholderArityOverride(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 parameterisedi18n(...)value in THIS FRAMEWORK's own two catalogues (src/main/resources/lang/en.json/zh.json) is formatted withString.format, neverMessageFormat(measured: 0{n}-style placeholders in either file, and 0 across anyi18n(...)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/*.ymlcatalogues 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 useString#replace-driven substitution exclusively, neverString.format.BRACE_TOKEN_PATTERNandmissingBracePlaceholder(java.lang.String, java.lang.String)exist to cover that population; this pattern andplaceholderArity(java.lang.String)continue to cover only the framework's own.Codex round 6, P2: deliberately narrower than the full
Formattergrammar --%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)%dconversion 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 realString.formatplaceholder 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
Matches a brace-style placeholder token as used by the MODULElangcatalogues this mechanism protects --{0}/{1}positional or{PLAYER}/{WORLD}named, substituted with plainString#replace, neverjava.util.Formatter. SeePLACEHOLDER_PATTERN's javadoc for the install-wide measurement establishing this is the modules' actual dialect, and #524 for the real-machine report this pattern andmissingBracePlaceholder(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_PATTERNwas 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_GSONA private, independent JSON reader for the D-05 placeholder-arity comparison only -- not a change toLanguage's own Gson usage, which stays entirely insideLanguage.java(untouched by this plan). -
ARITY_MAP_TYPE
-
language
-
version
-
pluginName
-
authors
-
loadAfter
-
minUltiToolsVersion
private final int minUltiToolsVersion -
mainClass
-
identifyString
-
resourceFolderPath
-
context
-
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 pluginversion- the version of the pluginauthors- the authors of the pluginloadAfter- the plugins which should be loaded before this pluginminUltiToolsVersion- the minimum version of UltiTools required by this pluginmainClass- the main class of the pluginresourceFolderPath- the path to the resource folder
-
-
Method Details
-
initializeLanguage
Initializes the language object- Returns:
- Language object
-
resolveLanguageCode
Resolves which language code to actually load, consultingLocalized.supported()beforecreateLanguageFromPath(String)picks a file (D-20/D-21/WIRE-10). Prefers the configured code; if it is absent from a non-emptysupported(), prefers"en"whensupported()contains it, otherwise the first entry insupported()'s iteration order. An emptysupported()is "no information" - the configured code is returned unchanged and nothing is logged.- Returns:
- the language code to actually load
-
createLanguageFromPath
Creates a Language object from the given resource folder path- Parameters:
folderPath- the resource folder path- Returns:
- Language object
-
resolveDiskLanguage
Resolves the on-disk language catalogue forcode, trying each ofLANGUAGE_EXTENSIONSin order and returning the first that exists -- independently of whichever extensionresolveJarLanguage(String)resolves for the same code (13-REVIEW round 5, own deep review of0ddc95f, finding 1).- Returns:
- the resolved on-disk language, or
nullif none of the extensions exist on disk
-
resolveJarLanguage
Resolves the jar-bundled language catalogue forcode, trying each ofLANGUAGE_EXTENSIONSin order and returning the first that exists -- independently of whichever extensionresolveDiskLanguage(String, String)resolves for the same code (13-REVIEW round 5, own deep review of0ddc95f, finding 1).- Returns:
- the resolved jar-bundled language, or
nullif none of the extensions exist in the jar
-
loadLanguageFromDisk
Reads<folderPath>/lang/<code><extension>if it exists, elsenull.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, andLanguage.withFallback(com.ultikits.ultitools.entities.Language)(unchanged) still owns filling a key this dictionary lacks entirely.16-05 / CodeQL
java/zipslipalert #11 (CWE-22):codeis only lightly restricted byLocalized.languageCodeOf(String)'s character allowlist when it comes from a scanned jar/directory entry -- and not restricted at all when it comes from a hostileconfig.yml language:value, sinceresolveLanguageCode()returns an unmatched configured code UNCHANGED wheneverLocalized.supported()is empty. This is therefore the last line of defence: before this file is read (or, deeper inresolveLanguageWithProvenance(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 guardsaveResources()already applies to extracted jar entries. A violation degrades to the samenull(“not loadable”) outcome as a missing file, never a thrown exception. -
isWithinDirectory
Verifies thatcandidate's canonical path is contained withinbaseDir's own canonical path -- the same Zip Slip guardsaveResources()already applies to extracted jar entries, generalized here for every other place an untrusted language code is turned into aFile(16-05 / CodeQLjava/zipslipalert #11, CWE-22). NeitherbaseDirnorcandidateneeds 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 propertysaveResources()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 directorycandidatemust resolve insidecandidate- the file built from an untrusted language code- Returns:
- whether
candidate's canonical path isbaseDir'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:- 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.
- 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. - 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.
- 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.
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 rootfile- the on-disk language file, already confirmed to existresourcePath- the extracted resource's path relative to the resource folder (e.g."lang/en.json"), matchingsaveResources()'s own keysextension- 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 ofresolveLanguageWithProvenance(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 (PMDNPathComplexity: once branch 2 in the caller gained its own content-comparison check for the Codex finding onUltiToolsPlugin.java:515, threadPRRT_kwDOIcF9Es6i2kao, this branch's own nestedwriteBytes/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 precomputedjarHash, so a partial/failed write can never be misreported as success (WR-01). Codex round 2, P2:record(...)swallows its ownIOExceptionand returnsvoid, 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 unconditionalreadLanguageFile(...), skipping the per-key placeholder-arity guard on EXACTLY the paths where a stale value is most likely --writeBytes(file, jarBytes)returningfalse(read-only file, symlink, unpreservable ownership, or an I/O failure mid-write) leaves the OLD disk bytes in place whilejarByteshas 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 throwsMissingFormatArgumentExceptionat format time -- extra bundled arguments over a narrower stale value are silently ignored byString.format(java.lang.String, java.lang.Object...), not an error. Applying the override UNCONDITIONALLY -- not only on thewriteBytes-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 existjarBytes- the current bundled bytes for this resource, already confirmed non-null by the callerjarHash-jarBytes's own SHA-256 digest, already computed by the callerdiskHash-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 placeresourceFolder- the module's resource folder root (the sidecar's own location)resourcePath- the extracted resource's path relative to the resource folderextension- the language file extension (".json",".yml"or".yaml")- Returns:
- the resolved language for this file
-
readLanguageFile
Reads an on-disk language file exactly as the pre-#441loadLanguageFromDiskdid -- extracted unchanged so both provenance branches that keep the disk file's own parse (branches 1 and 3 inresolveLanguageWithProvenance(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 inresolveLanguageWithProvenance(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 requiredString.formatargument 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.
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
Returns the highestString.formatargument POSITIONvaluerequires -- not the count of distinct positions used (Codex round 2, P2)."%2$s" alonerequires an args array of length (at least) 2, sinceString.formatdemands 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%sconversions in one value require arity two.%%(a literal percent) consumes no argument and is excluded --PLACEHOLDER_PATTERNitself (Codex round 6, P2) no longer matches anything outside%s/%d/%%at all, so%nand every otherFormatterconversion this framework never emits cannot reach this method to begin with.- Parameters:
value- a language value, ornull- Returns:
- the highest argument position
value'sString.formatconversions require, or0fornullor a value with none
-
missingBracePlaceholder
Reports whetherjarValuecontains a brace-style placeholder token (seeBRACE_TOKEN_PATTERN) thatdiskValuedoes not -- ONE-DIRECTIONALLY, and by TOKEN PRESENCE, not by count (#524). SeeapplyPlaceholderArityOverride(java.io.File, byte[], java.lang.String, java.lang.String)'s own javadoc for why this is deliberately asymmetric, unlikeplaceholderArity(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 keyjarValue- the current bundled value for the same key- Returns:
trueifjarValuehas at least one brace token absent fromdiskValue
-
bracePlaceholderTokens
Returns the distinct brace-style placeholder tokens (seeBRACE_TOKEN_PATTERN) found invalue-- 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 callscontainsAllon the result.- Parameters:
value- a language value, ornull- Returns:
- the distinct token names found, or an empty set for
nullor a value with none
-
readFlatDictionary
Reads a flatkey -> valuedictionary from a language file on disk, for the placeholder-arity comparison only --Languageitself exposes no way to enumerate its keys, so this is a small, independent read of the same file formats, not a change toLanguage'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
Reads a flatkey -> valuedictionary from jar-bundled bytes, mirroring the file overload above for the jar side of the comparison. -
readFlatDictionary
-
writeBytes
Writesbytestofile, 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 onResourceHashSidecar.java:320, threadPRRT_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 inPosixAttributePreserver.replaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener), shared withResourceHashSidecar#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 thinPosixAttributePreserver.ReplaceInPlaceListenersupplying this class's owngetLogger()/getPluginName()warning text; the full behavioural contract (what each refusal means, why each is conservative rather than best-effort) is documented onPosixAttributePreserver.replaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener)andPosixAttributePreserver.ReplaceInPlaceListenerthemselves, 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.jsonpointing at a shared store) is left completely untouched -- REFUSE, not write-through to the link's target. SeePosixAttributePreserver.ReplaceInPlaceListener.onSymbolicLink(). -
readEmbeddedResourceBytes
Reads the raw bytes ofresourcePath(e.g."lang/en.json") from this module's ownCodeSourcelocation, ornullif it is not present there. MirrorsloadLanguageFromJar(String, String)'s directory/jar dual branch exactly (13-REVIEW CR-01, issue #412 follow-up) but returns raw bytes instead of a parsedLanguage, so the D-05/D-06 provenance decision can hash and, in the overwrite branch, write those exact bytes to disk. -
readEmbeddedResourceBytesFromDirectory
The exploded-classpath (directoryCodeSource) branch ofreadEmbeddedResourceBytes(String), split out to keep that method's NPath complexity under the Codacy/PMD threshold once the 16-05 / CodeQLjava/zipslipalert #11 containment guard was added -- purely a structural split, the guard's own behaviour is unchanged.- Parameters:
location- the directoryCodeSourcelocationresourcePath- the resource path to read, e.g."lang/en.json"- Returns:
- the resource's raw bytes, or
nullif absent, escaping, or unreadable
-
loadLanguageFromJar
Readslang/<code><extension>from the module's ownCodeSourcelocation if present, elsenull. MirrorsLocalized.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 realUltiToolsPluginsubclass) has a directory as itsCodeSource, andLocalized.supported()already scans that directory directly -- this method must not disagree by unconditionally trying (and failing) to open the directory as aJarFilefirst.The resource path is built with
'/'for the jar-entry lookup andFile.separatorfor 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
Resolves aCodeSourcelocation as aFile, decoding any percent-escaped characters (spaces, non-ASCII, etc.) thatURL.getPath()does not decode on its own (13-REVIEW WR-03) -- passing an undecoded%20...path straight toFile(String)orJarFile(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 -- matchinggetInputStream()'s own try/catch(URISyntaxException) shape for the sameCodeSourcelocation -- for the rare case the location cannot be expressed as aURIat all.- Parameters:
location- theCodeSource.getLocation()URL, nevernull- Returns:
- the resolved location as a
File
-
parseLanguageResource
private static Language parseLanguageResource(BufferedReader reader, String extension) throws IOException Parses alang/*resource already opened as aBufferedReader-- shared by both the on-disk (exploded directory) and in-jar branches ofloadLanguageFromJar(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
Injects the IoC container created for this plugin.Called by
PluginManagerwhile a module is being loaded, beforeregisterSelf()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:
PluginManagerlives 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
Injects theDataScopecredentialPluginManagerminted for this module (D-17). Called byPluginManagerright after minting, beforewireAopruns -- the same lifecycle pointsetContext(com.ultikits.ultitools.context.SimpleContainer)documents.Deliberately still public, for the same reason
setContext(com.ultikits.ultitools.context.SimpleContainer)is:PluginManagerlives 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
- Returns:
- the config manager
-
getListenerManager
- Returns:
- the listener manager
-
getCommandManager
- Returns:
- the command manager
-
getPluginManager
- Returns:
- the plugin manager
-
initConfig
Initializes the configuration entity.- Throws:
IOException
-
diffGetAllConfigsOverride
private void diffGetAllConfigsOverride()Diffs aConfigurable.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 everyconfigFilePathwas already registered by the package scan is pure redundancy, logged atLevel.FINEonly. A non-empty override naming aconfigFilePaththe 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 aconfigFilePathauto-registration never registered
-
getInputStream
- Throws:
IOException
-
resolveJarFileNameForError
Best-effort resolution of this module's own JAR file name, for thename:-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
-
getConfigFile
-
getConfig
Description copied from interface:ConfigurableGet config by config type.- Specified by:
getConfigin interfaceConfigurable- Type Parameters:
T- Config type- Parameters:
configType- Config type- Returns:
- Config
-
getConfig
Description copied from interface:ConfigurableGet config by config path and config type.- Specified by:
getConfigin interfaceConfigurable- Type Parameters:
T- Config type- Parameters:
path- Config pathconfigType- Config type- Returns:
- Config
-
getConfigs
-
saveConfig
public <T extends AbstractConfigEntity> void saveConfig(String path, Class<T> configType) throws IOException Description copied from interface:ConfigurableSave config by config type.- Specified by:
saveConfigin interfaceConfigurable- Type Parameters:
T- Config type- Parameters:
path- Config pathconfigType- Config type- Throws:
IOException- IOException
-
saveResources
private void saveResources()Extracts this module's embeddedres/lang/configjar resources intoresourceFolderPath. 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 byloadLanguageFromDisk(java.lang.String, java.lang.String, java.lang.String)on a previous boot. -
getDataOperator
Gets data operator. Refuses outright ifdataClazzis not registered to this module (D-14) -- checked against the sameDataScopeminted for this module at load, via the same refusalDataStore.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 deprecatedgetOperator(UltiToolsPlugin, Class)overload directly -- soDataStore.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.dataScopeis normally non-null by the time any module calls this (set byPluginManagerright after minting, beforeregisterSelf()runs); thenullfallback below only covers a bare instance constructed outside the normalPluginManagerload flow (e.g. a test), where the deprecated overload's owncheckOwnership(...)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- ifdataClazzis not registered to this module
-
getLanguageCode
- Returns:
- language code
-
getLanguage
- Returns:
- the language
-
i18n
- Parameters:
str- the string to be localized- Returns:
- the localized string
-
i18n
Description copied from interface:LocalizedReturns a localized string for the given default text. Thecodeparameter is not honoured by the framework's own implementation --UltiToolsPluginoverrides this methodfinaland resolves directly against the module's already-loadedLanguage, discardingcodeentirely. Language selection happens once at load time, viaLocalized.supported(), not per call.stris returned unchanged when it is absent as a key from that already-loaded dictionary. -
isNewerVersionThan
- 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()isfinaland always calls this hook and the framework's own unregistration afterward -- a module cannot skip either by overridingunregisterSelf()itself, because that is no longer possible (D-01). The default body does nothing; override this method, notunregisterSelf(), 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 unguardedsupercall could. If a cleanup step also fails, that failure does not replace this hook's own exception: it is attached to it viaThrowable.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:IPluginUltiTools will call this method when unregistering this plugin module, similar toJavaPlugin.onDisable()- Specified by:
unregisterSelfin interfaceIPlugin
-
runUnregisterStep
Runs oneunregisterSelf()cleanup step, collecting any failure ontopreviousFailureinstead of letting it replace or be replaced by another step's failure.stepcan only throw an unchecked exception --onUnregister(),CommandManager#unregisterAll, andListenerManager#unregisterAllall declare no checked exceptions -- so catchingRuntimeException | Errorhere covers every real case without the width of catchingThrowable(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, ornullif every earlier step (if any) succeededstep- the cleanup step to run- Returns:
previousFailure, withstep's own failure (if any) attached to it viaThrowable.addSuppressed(java.lang.Throwable); or, ifpreviousFailurewasnull,step's own failure, ornullifstepcompleted 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,@ConditionalOnConfigdrift 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()isfinaland always runs its own steps first; a module cannot skip them by overridingreloadSelf()itself, because that is no longer possible (D-01). The default body does nothing; override this method, notreloadSelf(), 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
@ConditionalOnConfigdrift: 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@Scheduledtasks and@CmdCDcooldowns pick up their reloaded values (#531; seePluginManager#applyReloadedConfigBindings).finaland always runs its own steps, then logs one framework-owned INFO line naming this module (D-03), then callsonReload()-- a module can no longer skip any of this by overridingreloadSelf()itself, because that override point no longer exists (D-01).- Specified by:
reloadSelfin interfaceIPlugin
-
getLogger
- Returns:
- plugin logger
-