Class UltiToolsPlugin
- All Implemented Interfaces:
Configurable,IPlugin,Localized
- Version:
- 1.0.0
- Author:
- wisdomme
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionprivate static final classA custom language file as last parsed for comment recognition;languageis null when unreadable. -
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 final Map<String, UltiToolsPlugin.CustomCatalogue> The operator's custom language files last read for comment recognition (seecustomCatalogueTexts(java.lang.String)), by path, each with the modification time and size it was read at, so a file is parsed again only after it changed.private DataScopeprivate final Stringprivate Language(package private) static final StringFramework i18n key for the partial-reload reason a per-module reload records when thelanguagesetting in the framework'sconfig.ymlon disk differs from the one the framework runs with (#502).private static final String[]Language file extensions, in the order they are tried.(package private) static final StringFramework i18n key for the one line logged per official language file restored to the bundled version (#459; #608 and the maintainer decision of 2026-10-04, "official language file edited in place": restored at every start, the edited copy backed up, the operator told to copy, rename and select instead).private booleanTrue only while the constructor resolves this module's language (#460): the provenance decision is computed but nothing is written, renamed or recorded, and nothing is logged.private booleanTrue only whilecommitLanguageProvenance()migrates the language files of languages other than the one being resolved (Codex review of #566): the provenance writes happen, but no dictionary is built for them, so no per-key placeholder warning is logged about a file that is not in use.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 for the SEVERE linereloadSelf()logs, naming the module and the cause, when any of its steps or the module's ownonReload()throws (#509).(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).(package private) static final StringFramework i18n key for the WARNING line logged instead ofRELOAD_LOG_MESSAGE_KEYwhen the module's reload hook recorded parts that did not reload in itsReloadReport(#529).(package private) static final StringThe partial reason in the reportreloadWithReport()returns when the call was refused because it was made off the server thread (#538).private StringThe language catalogues the module's jar ships, read once on first use (seeshippedCatalogueTexts(java.lang.String)).private static final LoggerReceives nothing:languageLog()hands it out while a language is resolved without writing (#460), so a construction-time pass logs nothing and the committing pass after the load gates logs everything exactly once.private 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.bundledDictionary(String code) The jar-bundled dictionary ofcode, from the first ofLANGUAGE_EXTENSIONSthe jar ships, for the placeholder comparison of a custom file; empty when the jar ships none.final voidCommits this module's language provenance decision (#460): re-resolves the language with its writes enabled -- the refresh of an untouched language file and its provenance record -- and makes the result this module's language.private static booleanconversionMismatch(String ownValue, String officialValue) Reports whetherownValueandofficialValueuse a differentString.formatconversion for an argument position both of them use (#615 item 3; maintainer decision 2026-10-06):%dwhere the other has%sat the same position, explicit (%2$d) or implied by the order of unindexed conversions.private FilecopyToFreshBackup(File file) Copiesfileto the first backup name that does not exist yet --<name>.bak, then<name>.1.bak,<name>.2.bak-- never overwriting anything: a name is skipped when any file or link exists there, and the copy itself refuses an existing target, so a name taken in between is skipped too.private LanguagecreateLanguageFromPath(String folderPath) Creates the language from the given resource folder: the operator's custom language file when one is selected and present, falling back key by key to the official language it is based on -- the official file on disk, falling back to the jar's copy (#608).private LanguagecreateOfficialLanguage(String folderPath, String resolvedCode, boolean warnIfMissing) Resolves the official languageresolvedCodefrom disk and jar, as before #608.private LanguagecustomCatalogue(File langDir, File file, Set<String> shippedCodes) The parsed custom filefile, ornullwhen it is not a custom language file or not readable.The texts the operator's custom language files in this module'slang/folder hold forkey(#615 item 1; contract-batching decision of 2026-10-06): one entry per custom file that holds the key, in file name order.private static StringcustomLanguageName(String configured, String official) The custom language name in use (#608): the configured name when it is not the official language it resolves to and is a safe language-code token (Localized.isSafeLanguageCode(java.lang.String)), so it can never name a path outsidelang/; otherwisenull.private voiddeleteOwnBackup(File backup) Deletes the backupcopyToFreshBackup(java.io.File)created in this same replacement attempt, after the replacement itself failed.private static StringdescribeFailure(Throwable failure) A failure's message for a one-line report, or its class name when it carries none.private 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 StringThelanguagesetting ofplugins/UltiTools/config.ymlas the framework runs with it: an official code such aszh, or the name of an operator's custom language file such aszh-myserver.final <T extends BaseDataEntity<String>>
DataOperator<T> getDataOperator(Class<T> dataClazz) Gets data operator.private InputStreamfinal Languagefinal StringThe official language this module's messages are based on: a language code this module ships, such asenorzh.static ListenerManagerstatic PluginManagerfinal StringReturns a localized string for the given default text.private voidInitializes the configuration entity.private LanguageResolves this module's language during construction without writing anything (#460): no language file is refreshed, replaced or backed up, no provenance is recorded, and nothing is logged.booleanisNewerVersionThan(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(boolean)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 LanguagejarCopyAsIfExtracted(String resourcePath, String extension) The language alang/file would hold once extracted: the jar's copy ofresourcePath, parsed asreadLanguageFile(java.io.File, java.lang.String)parses the extracted file;nullwhen the jar does not ship it (#567 item 1).private PluginLoggerThe logger the language-resolution methods write through: this module's logger, except during the construction-time pass (#460), when it discards everything so each message is logged once, by the committing pass.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 StringlostPlaceholderReason(String ownValue, String officialValue) WhyownValuelost a placeholder relative toofficialValue, ornullif it did not.private voidmigrateBundledLanguageFile(String code, String extension) private voidApplies the provenance decision to every language file this module's jar bundles and the disk holds, not only the configured language's (Codex review of #566): on the first start after an upgrade, an unrecordedlang/zh.jsonof a server runningenis replaced and recorded too (#459), so an edit made to it afterwards is kept as a customisation when the operator later switches language.private 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).private booleannamesConfigDirectory(String configFilePath) private static Loggerprivate StringofficialLanguageFor(String configured, boolean warn) The official languageconfiguredis based on (#608).protected voidonReload()Extension point for a module's own reload work.protected voidonReload(ReloadReport report) Extension point for a module's own reload work, with a way to report a partial reload (#529).protected voidExtension point for a module's own unload cleanup.overrideLostPlaceholders(Map<String, String> own, Map<String, String> official, BiConsumer<String, String> mismatch) The two-detector placeholder comparison ofapplyPlaceholderArityOverride(java.io.File, byte[], java.lang.String, java.lang.String)(#441, #524), as a pure function shared with an operator's custom language file (#608) and the framework's own custom file: every key ofownwhose value lost a placeholder relative toofficial's value for the same key -- a different%s/%darity (placeholderArity(java.lang.String)), a malformed format-argument index, a different conversion at a position both values use (conversionMismatch(java.lang.String, java.lang.String), #615 item 3), or a{TOKEN}the official value has andown's lacks (missingBracePlaceholder(java.lang.String, java.lang.String)) -- is replaced by the official value, andmismatchis told the key and the reason phrase (never either value).private static LanguageparseCustomCatalogue(File file, String extension) 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 StringThe framework'slanguagesetting as it is inconfig.ymlon disk now, when it differs from the value the framework runs with (#502).private ReloadReportThe body ofreloadSelf()andreloadWithReport().private static intplaceholderArity(String value) Returns the highestString.formatargument POSITIONvaluerequires -- not the count of distinct positions used (Codex round 2, P2).placeholderConversions(String value) TheString.formatconversions ofvalueby argument position: an explicit index (%2$s) names its position, an unindexed conversion takes the next ordinary position, asjava.util.Formattercounts them.private voidPrepares converters and checks only enabled auto-config entities before resources are extracted.private LanguagereadCustomLanguageFile(String folderPath, String name, String officialCode) Reads the operator's custom language file<folderPath>/lang/<name><extension>, trying each ofLANGUAGE_EXTENSIONSin order (#608).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.private booleanrecordReplacedFile(File file, File resourceFolder, String resourcePath) Recordsfile's current hash and reads the record back.private voidrecordUnlessDryRun(File resourceFolder, String resourcePath, String hash) RecordshashforresourcePath, except during the construction-time pass, which writes nothing (#460).final voidReload this plugin's configuration and language files.final ReloadReportReloads this module exactly asreloadSelf()does, and returns what its reload hook reported (#529).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 the official language code to load, consultingLocalized.supported()beforecreateLanguageFromPath(String)picks a file (D-20/D-21/WIRE-10), and logs a warning when the configured name has no official base.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 voidrestoreFromBackup(File backup, File file) Moves the backup this attempt created back overfile, restoring the original bytes, after the provenance record could not be written.private LanguagerestoreOfficialLanguageFile(File file, byte[] jarBytes, File resourceFolder, String resourcePath, String extension) Branches 2 and 4 ofresolveLanguageWithProvenance(java.lang.String, java.io.File, java.lang.String, java.lang.String): an official language file whose bytes differ from the jar's copy -- an unrecorded one (#459, maintainer 2026-09-29, question 3 option 2) or one edited in place after it was recorded (#608, maintainer decision 2026-10-04: official language files are framework-owned and restored at every start) -- is replaced by the jar's copy and recorded; the previous file is kept as a backup beside it and one catalogue line names both and tells the operator to copy, rename and select a custom file instead.private static <T extends Throwable>
voidrethrowUnchanged(Throwable failure) Throwsfailureitself -- the same instance, of its own type, unwrapped -- although it may be a checked exception this method's callers do not declare.private voidrunReloadSteps(ReloadReport report) The reload steps, in order, ending with the module's own hook.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) Saves this module's configuration registered atpaththroughAbstractConfigEntity.save(): only the settings the module changed since the last load or save are written, and only where the file still holds the value the module started from; every other byte of the file stays as the operator left it, and a change that cannot be written stays in memory and is named in one warning.private voidsaveResources(boolean languageFiles) voidsetContext(SimpleContainer context) Injects the IoC container created for this plugin.voidsetDataScope(DataScope scope) Injects theDataScopecredentialPluginManagerminted for this module (D-17).The texts the language catalogues this module's jar ships hold forkey: one entry per shipped catalogue - every language code under the jar'slang/, every extension the loader reads - that holds the key, in code order.final voidUnloads this module's own registrations: runsonUnregister(), then unregisters this module's commands, then its listeners.private voidwarnLostPlaceholder(String key, String fileLabel, String reason) 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. -
SILENT_LANGUAGE_LOGGER
Receives nothing:languageLog()hands it out while a language is resolved without writing (#460), so a construction-time pass logs nothing and the committing pass after the load gates logs everything exactly once. -
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:
-
LANGUAGE_REPLACED_LOG_KEY
Framework i18n key for the one line logged per official language file restored to the bundled version (#459; #608 and the maintainer decision of 2026-10-04, "official language file edited in place": restored at every start, the edited copy backed up, the operator told to copy, rename and select instead). Arguments: the restored file, the module, the backup. Package-private so a test can assert both shipped catalogues translate it.- See Also:
-
RELOAD_FAILED_LOG_MESSAGE_KEY
Framework i18n key for the SEVERE linereloadSelf()logs, naming the module and the cause, when any of its steps or the module's ownonReload()throws (#509). The success lineRELOAD_LOG_MESSAGE_KEYis never logged for that reload. Package-private for the same catalogue-coverage test as that key.- See Also:
-
RELOAD_PARTIAL_LOG_MESSAGE_KEY
Framework i18n key for the WARNING line logged instead ofRELOAD_LOG_MESSAGE_KEYwhen the module's reload hook recorded parts that did not reload in itsReloadReport(#529). Package-private for the same catalogue-coverage test as that key.- See Also:
-
RELOAD_REFUSED_OFF_THREAD_REASON
The partial reason in the reportreloadWithReport()returns when the call was refused because it was made off the server thread (#538). Not an i18n key: nothing reaches the operator through it on the framework's own reload paths, which run on the server thread; it is for a module that callsreloadWithReport()itself.- See Also:
-
LANGUAGE_CHANGE_PENDING_KEY
Framework i18n key for the partial-reload reason a per-module reload records when thelanguagesetting in the framework'sconfig.ymlon disk differs from the one the framework runs with (#502). Package-private for the catalogue-coverage test.- 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
-
shippedCatalogues
The language catalogues the module's jar ships, read once on first use (seeshippedCatalogueTexts(java.lang.String)). -
customCatalogues
The operator's custom language files last read for comment recognition (seecustomCatalogueTexts(java.lang.String)), by path, each with the modification time and size it was read at, so a file is parsed again only after it changed. -
version
-
pluginName
-
authors
-
loadAfter
-
minUltiToolsVersion
private final int minUltiToolsVersion -
mainClass
-
identifyString
-
resourceFolderPath
-
context
-
dataScope
-
languageDryRun
private boolean languageDryRunTrue only while the constructor resolves this module's language (#460): the provenance decision is computed but nothing is written, renamed or recorded, and nothing is logged.commitLanguageProvenance()re-resolves with writes once the load gates accepted this candidate. Construction is single-threaded, so a plain field is enough. -
languageMigrationOnly
private boolean languageMigrationOnlyTrue only whilecommitLanguageProvenance()migrates the language files of languages other than the one being resolved (Codex review of #566): the provenance writes happen, but no dictionary is built for them, so no per-key placeholder warning is logged about a file that is not in use.
-
-
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
Resolves this module's language during construction without writing anything (#460): no language file is refreshed, replaced or backed up, no provenance is recorded, and nothing is logged. The returned language is the one the commit will produce when its writes succeed;commitLanguageProvenance()performs the writes and replaces it once the load gates have accepted this candidate, so a rejected candidate leaves the shared files untouched.- Returns:
- the language this module will use once its provenance decision is committed
-
commitLanguageProvenance
@Internal public final void commitLanguageProvenance()Commits this module's language provenance decision (#460): re-resolves the language with its writes enabled -- the refresh of an untouched language file and its provenance record -- and makes the result this module's language. Called byPluginManageronce the load gates have accepted this candidate, before its container is assembled; construction only computed the decision, so a candidate the gates reject never writes to the language files it shares with the accepted version.It is also the only point that extracts the module's
lang/resources the disk lacks, with their hashes recorded as at any extraction (#567 item 1): construction extracts onlyconfig/andres/, and resolves a missinglang/file from the jar's copy without writing it. A copy the load gates reject never reaches this method, so it cannot write a language file.Not part of the module-facing API. Public only because
PluginManagerlives in another package, likesetContext(com.ultikits.ultitools.context.SimpleContainer). Calling it again re-runs the same resolution a reload's language step runs, so it is harmless but pointless.- Since:
- 6.3.0
-
migrateBundledLanguageFiles
private void migrateBundledLanguageFiles()Applies the provenance decision to every language file this module's jar bundles and the disk holds, not only the configured language's (Codex review of #566): on the first start after an upgrade, an unrecordedlang/zh.jsonof a server runningenis replaced and recorded too (#459), so an edit made to it afterwards is kept as a customisation when the operator later switches language. The configured language is resolved afterwards, as before; its file is then already migrated. Codes come from the jar's ownlang/entries.Every dictionary builder reachable from here --
readLanguageFile(java.io.File, java.lang.String)andapplyPlaceholderArityOverride(java.io.File, byte[], java.lang.String, java.lang.String)-- returns an empty dictionary whilelanguageMigrationOnlyis set, so no unused catalogue is parsed (Codex review of #566, run 2). -
migrateBundledLanguageFile
-
languageLog
The logger the language-resolution methods write through: this module's logger, except during the construction-time pass (#460), when it discards everything so each message is logged once, by the committing pass. -
newSilentLogger
-
resolveLanguageCode
Resolves the official language code to load, consultingLocalized.supported()beforecreateLanguageFromPath(String)picks a file (D-20/D-21/WIRE-10), and logs a warning when the configured name has no official base. SeeofficialLanguageFor(String, boolean)for the rule.- Returns:
- the official language code to load
-
officialLanguageFor
The official languageconfiguredis based on (#608). An official code this module ships (Localized.supported()) is its own base; a custom name is based on the shipped code it starts with followed by a hyphen (zh-myserveronzh, seeLocalized.officialLanguageOf(java.lang.String, java.util.List<java.lang.String>)); any other name falls back to"en"when the module ships it, otherwise to the first shipped code, with one warning whenwarnis set. An emptysupported()is "no information":configuredis returned unchanged and nothing is logged, as before 6.3.0. -
customLanguageName
The custom language name in use (#608): the configured name when it is not the official language it resolves to and is a safe language-code token (Localized.isSafeLanguageCode(java.lang.String)), so it can never name a path outsidelang/; otherwisenull. A module that ships no languages has no official base and no custom name. -
readCustomLanguageFile
Reads the operator's custom language file<folderPath>/lang/<name><extension>, trying each ofLANGUAGE_EXTENSIONSin order (#608).Read-only by construction: a custom file belongs to the operator (maintainer decision 2026-10-04), so this method never calls
loadLanguageFromDisk(java.lang.String, java.lang.String, java.lang.String)-- whose provenance step can refresh, restore or back up a file -- and never writes, renames, backs up or records anything. The module jar never ships a file under a custom name either, sosaveResources(boolean)andmigrateBundledLanguageFiles()never reach one. The canonicallang/containment guard applies as for every other language read.A key whose custom value lost a placeholder relative to the official base's bundled value uses the bundled value, with one warning naming the file and the key (
overrideLostPlaceholders(java.util.Map<java.lang.String, java.lang.String>, java.util.Map<java.lang.String, java.lang.String>, java.util.function.BiConsumer<java.lang.String, java.lang.String>); gate-1 review R1-2 of plan 17-69): a custom file written for an older release must not make a message fail to format after an upgrade.- Returns:
- the custom language, or
nullwhen no such file exists or none is readable
-
createLanguageFromPath
Creates the language from the given resource folder: the operator's custom language file when one is selected and present, falling back key by key to the official language it is based on -- the official file on disk, falling back to the jar's copy (#608).- Parameters:
folderPath- the resource folder path- Returns:
- Language object
-
createOfficialLanguage
private Language createOfficialLanguage(String folderPath, String resolvedCode, boolean warnIfMissing) Resolves the official languageresolvedCodefrom disk and jar, as before #608.- Parameters:
warnIfMissing- whether to log that no language is loadable (not when a custom file is in use)- Returns:
- the official language, or
nullwhen none is loadable
-
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
-
bundledDictionary
The jar-bundled dictionary ofcode, from the first ofLANGUAGE_EXTENSIONSthe jar ships, for the placeholder comparison of a custom file; empty when the jar ships none. -
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
-
shippedCatalogueTexts
The texts the language catalogues this module's jar ships hold forkey: one entry per shipped catalogue - every language code under the jar'slang/, every extension the loader reads - that holds the key, in code order. The config layer uses them to recognise a comment line the framework wrote in any language (maintainer decision 2026-10-04, "only the framework's own comments are rewritten", #604).Read-only by construction: it reads the module's own code source through
loadLanguageFromJar(java.lang.String, java.lang.String)and nothing else - never the extracted language files under the module's folder, the resource hash record or a language backup - so it performs none of the language-provenance writes a language load makes, and logging a catalogue it cannot read is its only effect. The catalogues are read once per module instance: the jar does not change while the module is loaded.- Parameters:
key- the catalogue key- Returns:
- the shipped texts for
key, possibly empty, nevernull
-
shippedCatalogues
-
customCatalogueTexts
The texts the operator's custom language files in this module'slang/folder hold forkey(#615 item 1; contract-batching decision of 2026-10-06): one entry per custom file that holds the key, in file name order. A custom file is a language file --.json,.ymlor.yaml-- whose name is a safe language-code token (Localized.isSafeLanguageCode(java.lang.String)) that the module does not ship (Localized.supported(), which a module that overrides it must keep equal to its jar'slang/codes), such aszh-myserver.yml: the files an operator selects as a custom language (#608). The config layer adds them to the texts it recognises as framework-written comments, so a comment the framework wrote while the operator's custom language was selected keeps following later language switches.Read-only by construction, like
readCustomLanguageFile(java.lang.String, java.lang.String, java.lang.String): it lists the folder and parses the files, and never writes, renames, backs up, refreshes or records anything -- no provenance step and no placeholder guard, and it logs nothing of its own. A file that cannot be parsed contributes no text; for a.yml/.yamlfile Bukkit's YAML reader itself logs the parse failure, even when the file is not the selected language (a documented limitation, gate 2 of PR #636: an unusable file left inlang/is the operator's to fix or remove). A file is parsed again only after its modification time or size changed, so the texts follow the operator's edits; a file replaced by one of the same size with its old modification time kept is noticed at the next start (documented limitation, same review).Only the operator's CURRENT custom texts are known: no record of earlier renderings is kept (maintainer decision of 2026-10-06, option A). A comment written from a text the operator has since edited in the custom file is therefore the operator's own and stays as it is.
- Parameters:
key- the catalogue key- Returns:
- the custom files' texts for
key, possibly empty, nevernull - Since:
- 6.3.0
-
customCatalogue
The parsed custom filefile, ornullwhen it is not a custom language file or not readable. -
parseCustomCatalogue
- Throws:
IOException
-
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(boolean)already applies to extracted jar entries. A violation degrades to the samenull(“not loadable”) outcome as a missing file, never a thrown exception. -
jarCopyAsIfExtracted
The language alang/file would hold once extracted: the jar's copy ofresourcePath, parsed asreadLanguageFile(java.io.File, java.lang.String)parses the extracted file;nullwhen the jar does not ship it (#567 item 1). Reads the jar only; writes nothing. -
isWithinDirectory
Verifies thatcandidate's canonical path is contained withinbaseDir's own canonical path -- the same Zip Slip guardsaveResources(boolean)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 an edit -- see the branch-2 code comment below (Codex
finding, thread
PRRT_kwDOIcF9Es6i2kao, P2) for why. When the disk bytes equal the jar's the record is repaired; when they differ, the official file was edited in place and is restored like branch 4 (#608, maintainer decision 2026-10-04: restored at every start, edited copy backed up, one log line). - 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: the
file is replaced by the jar's copy and recorded, the old file kept as a backup and named
in the log (#459, maintainer 2026-09-29, question 3 option 2; see
restoreOfficialLanguageFile(java.io.File, byte[], java.io.File, java.lang.String, java.lang.String)). If the replacement fails the file is kept and the per-key placeholder-arity override applies, as before.
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. This is also why an operator's custom language file (#608) can never reach any branch: a custom name is by definition not a language the jar ships, and custom files are read only byreadCustomLanguageFile(java.lang.String, java.lang.String, java.lang.String).Every branch above derives its 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). Since #459, branch 4 no longer concludes "customisation" at all, and since #608 neither does branch 2: an official file whose bytes differ from the jar's is restored, with a backup. 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"
-
recordUnlessDryRun
RecordshashforresourcePath, except during the construction-time pass, which writes nothing (#460). -
restoreOfficialLanguageFile
private Language restoreOfficialLanguageFile(File file, byte[] jarBytes, File resourceFolder, String resourcePath, String extension) Branches 2 and 4 ofresolveLanguageWithProvenance(java.lang.String, java.io.File, java.lang.String, java.lang.String): an official language file whose bytes differ from the jar's copy -- an unrecorded one (#459, maintainer 2026-09-29, question 3 option 2) or one edited in place after it was recorded (#608, maintainer decision 2026-10-04: official language files are framework-owned and restored at every start) -- is replaced by the jar's copy and recorded; the previous file is kept as a backup beside it and one catalogue line names both and tells the operator to copy, rename and select a custom file instead. It is only ever called for a resource the jar ships, so it can never touch an operator's custom-named file. During the construction-time pass (#460) it writes nothing and returns the jar's dictionary, the language a replacement yields.Steps, each undone if a later one fails, so a failure at any step leaves the original file in place, byte-identical, with nothing recorded and no backup left behind:
- copy the file to a backup name that does not exist yet (
copyToFreshBackup(java.io.File)). A copy rather than a rename, so the language file is never absent: a crash here leaves the original in place and, at worst, one extra backup; - write the jar's bytes to a temporary file in the same folder and move it over the
original atomically (
writeBytes(java.io.File, byte[]), the refresh path branch 1 uses; it refuses a read-only or symbolic-link file, which an operator pinned). On failure the backup this call created is deleted -- it is a copy, so nothing is lost; - record the new hash and read the record back. On failure the backup is moved back over the file, restoring the original bytes;
- log one line naming the file and its backup.
- copy the file to a backup name that does not exist yet (
-
copyToFreshBackup
Copiesfileto the first backup name that does not exist yet --<name>.bak, then<name>.1.bak,<name>.2.bak-- never overwriting anything: a name is skipped when any file or link exists there, and the copy itself refuses an existing target, so a name taken in between is skipped too. The naming is shared with the framework's own language files (OfficialLanguageFiles.copyToFreshBackup(java.io.File)).- Returns:
- the backup created, or
nullif none could be (the reason is logged)
-
recordReplacedFile
Recordsfile's current hash and reads the record back.- Returns:
- whether the record now holds exactly that hash
-
deleteOwnBackup
Deletes the backupcopyToFreshBackup(java.io.File)created in this same replacement attempt, after the replacement itself failed. It is a copy of a file that is still in place, so nothing is lost, and leaving it would add one more backup on every start that fails the same way. -
restoreFromBackup
Moves the backup this attempt created back overfile, restoring the original bytes, after the provenance record could not be written. Atomic where the filesystem supports it. -
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.
-
overrideLostPlaceholders
@Internal public static Map<String,String> overrideLostPlaceholders(Map<String, String> own, Map<String, String> official, BiConsumer<String, String> mismatch) The two-detector placeholder comparison ofapplyPlaceholderArityOverride(java.io.File, byte[], java.lang.String, java.lang.String)(#441, #524), as a pure function shared with an operator's custom language file (#608) and the framework's own custom file: every key ofownwhose value lost a placeholder relative toofficial's value for the same key -- a different%s/%darity (placeholderArity(java.lang.String)), a malformed format-argument index, a different conversion at a position both values use (conversionMismatch(java.lang.String, java.lang.String), #615 item 3), or a{TOKEN}the official value has andown's lacks (missingBracePlaceholder(java.lang.String, java.lang.String)) -- is replaced by the official value, andmismatchis told the key and the reason phrase (never either value). Keysownlacks are left out:Language.withFallback(com.ultikits.ultitools.entities.Language)resolves them. In memory only: nothing is read or written here.Not part of the module-facing API. Public only because the framework class reads its own custom file in another package.
- Parameters:
own- the operator's dictionary (an edited official file, or a custom language file)official- the bundled dictionary of the same, or the base, languagemismatch- receives(key, reason)for each replaced key; the reason completes the sentence "Language key 'k' in 'file' ... " ("has a different placeholder count than the current bundled version"and the like)- Returns:
ownwith each such key replaced by the official value- Since:
- 6.3.0
-
lostPlaceholderReason
WhyownValuelost a placeholder relative toofficialValue, ornullif it did not. -
warnLostPlaceholder
-
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
-
conversionMismatch
Reports whetherownValueandofficialValueuse a differentString.formatconversion for an argument position both of them use (#615 item 3; maintainer decision 2026-10-06):%dwhere the other has%sat the same position, explicit (%2$d) or implied by the order of unindexed conversions. Runs only afterplaceholderArity(java.lang.String)found the same count, and sees exactly what that count sees (PLACEHOLDER_PATTERN:%sand%d, optionally indexed;%%is not a conversion).The rule (decided by plan 17-77, revision 0): for every position BOTH values use, the set of conversion characters at that position must be equal. A position only one value uses is left to the count comparison, so a value that keeps the bundled count but drops or reorders arguments with explicit indices keeps the outcome it had before this check existed; conversions in a different order with the same explicit indices are the same conversions. Equality, not "would it throw":
%sfor a bundled%dwould format, but the maintainer's decision compares the types, so the text that reaches a player is the one whose argument types the module was written for.- Parameters:
ownValue- the operator's valueofficialValue- the bundled value of the same key- Returns:
trueif a shared position carries different conversions
-
placeholderConversions
TheString.formatconversions ofvalueby argument position: an explicit index (%2$s) names its position, an unindexed conversion takes the next ordinary position, asjava.util.Formattercounts them. Called only afterplaceholderArity(java.lang.String)parsed the same value, so a malformed index has already been reported. -
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. Delegates toCodeSourceLocations.toFile(URL), the resolverLocalized.scanLangResources(URL)also uses, so language discovery and the dictionary reader cannot disagree about where the module's JAR is (#676 case 26); that resolver falls back to the previous raw-path rule when the location cannot be read as afile:URI.- 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
-
prepareConfigConverters
private void prepareConfigConverters()Prepares converters and checks only enabled auto-config entities before resources are extracted. -
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.A directory-backed entity is the exception to the path comparison. Auto-registration expands a
configFilePaththat names a directory undergetResourceFolderPath()(the sameisDirectory()testConfigManager#registerapplies) into one registration per file inside it, and registers nothing for an empty directory, so the unexpanded path is never a registered key. Such an override entity counts as found when its class is among the config classes package-scan auto-registration selected - the sameConverterRegistry.prepareSelectedConfigs(com.ultikits.ultitools.abstracts.UltiToolsPlugin, java.lang.String[], java.lang.ClassLoader)selection over the same scan packages and class loaderinitConfig()hands toConfigManager#registerAll. An entity class the scan did not select is still refused. This method only reads registrations, the selected classes and whether a path is a directory; it never writes or creates a configuration file.- Throws:
ConfigurationException- if the override names aconfigFilePathauto-registration never registered
-
namesConfigDirectory
- Parameters:
configFilePath- aconfigFilePathfrom aConfigurable.getAllConfigs()override- Returns:
- whether it names an existing directory under this module's resource folder, the
test
ConfigManager#registeruses to expand a directory-backed entity
-
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 Saves this module's configuration registered atpaththroughAbstractConfigEntity.save(): only the settings the module changed since the last load or save are written, and only where the file still holds the value the module started from; every other byte of the file stays as the operator left it, and a change that cannot be written stays in memory and is named in one warning. It cannot overwrite operator content: a value the operator edited, deleted or wrote unusably is never written over (maintainer decision 2026-10-04). Use it for a change the operator asked for through the module, or for shipped text re-rendered after a language switch.- Specified by:
saveConfigin interfaceConfigurable- Type Parameters:
T- the configuration entity type- Parameters:
path- the registered configuration file path, for exampleconfig/config.ymlconfigType- the configuration entity class- Throws:
IOException- if publishing the verified file fails
-
saveResources
private void saveResources(boolean languageFiles) 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.Two halves (#567 item 1). The constructors pass
false:config/andres/only, becauseinitConfig()reads them right after.commitLanguageProvenance()passestrue:lang/only, once the load gates accepted this copy. A copy the gates reject is never committed, so it writes no language file; its construction-time language reads the jar's copy instead (seeloadLanguageFromDisk(java.lang.String, java.lang.String, java.lang.String)).- Parameters:
languageFiles-truefor thelang/resources,falseforconfig/andres/
-
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
The official language this module's messages are based on: a language code this module ships, such asenorzh.Use it for behaviour that depends on the language, such as choosing which shipped text to write into a configuration file or which example content to create. An operator may select a custom language file in
plugins/UltiTools/config.yml: a copy of an official file under a new name that starts with the official code and a hyphen, such aszh-myserver. For that setting this method returnszh. A name that starts with no shipped code resolves toenwhen the module ships it, otherwise to its first shipped code. A module that ships no language files gets the configured name unchanged. The configured name itself isgetConfiguredLanguage().- Returns:
- the official language code, or
nullif no language is configured and the module ships no language files - Since:
- 6.3.0 returns the official base of a custom language name (#608); before 6.3.0 it returned the configured name
-
getConfiguredLanguage
Thelanguagesetting ofplugins/UltiTools/config.ymlas the framework runs with it: an official code such aszh, or the name of an operator's custom language file such aszh-myserver. Messages come fromlang/<name>.*in this module's folder when that file exists, and from the official languagegetLanguageCode()returns for every key the file lacks.- Returns:
- the configured language name, or
nullif none is configured - Since:
- 6.3.0
-
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.This hook may throw anything: an unchecked exception, an
Error, or a checked exception it does not declare (Lombok@SneakyThrows, or a rethrow helper). Whatever it 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. The exception reaches the caller unchanged: the same instance, of the same type, never wrapped. 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()Unloads this module's own registrations: runsonUnregister(), then unregisters this module's commands, then its listeners.final, so a module cannot skip either cleanup step.Every step runs whatever an earlier step threw, including a checked exception the hook does not declare. When a step failed, the first failure, in that order, is rethrown afterwards as the same instance and type, never wrapped; every later failure is attached to it via
Throwable.addSuppressed(java.lang.Throwable). A checked exception therefore reaches the caller although this method declares none.- 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.onUnregister(),CommandManager#unregisterAllandListenerManager#unregisterAlldeclare no checked exceptions, but module code can still throw one without declaring it (Lombok@SneakyThrows, or a rethrow helper). Catching onlyRuntimeException | Errorlet such an exception escape this barrier and skip every later step, leaving the module's commands and listeners registered after its unload. So this catchesException | Error: everyThrowablea step can really throw (this is a deliberate cleanup barrier, not a broad catch:unregisterSelf()always runs every remaining step and always rethrows exactly one collected failure unchanged, 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
-
rethrowUnchanged
Throwsfailureitself -- the same instance, of its own type, unwrapped -- although it may be a checked exception this method's callers do not declare.unregisterSelf()uses it to hand a hook's undeclared checked exception to its caller unchanged.- Type Parameters:
T- inferred as an unchecked type at the call site, so nothrowsclause is needed- Parameters:
failure- the collected failure to rethrow- Throws:
T- always:failure
-
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 and@ConditionalOnConfigdrift report (D-02) -- so a real-work override sees the already-reloaded configuration rather than the stale one. The framework-owned per-module reload line (D-03) is logged only after this hook returned; if it throws, a failure line naming the module is logged instead and the exception propagates to the caller (#509).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. -
onReload
Extension point for a module's own reload work, with a way to report a partial reload (#529).Called by
reloadSelf()andreloadWithReport()at the pointonReload()is documented to run, with a freshReloadReport. A module that reloads only part of what it should records each part that did not reload withReloadReport.partial(String)and returns normally; the framework then reports the reload as partial, naming those parts, instead of as a success. The default body callsonReload(), so a module that overrides only that method behaves exactly as before 6.3.0. Override one of the two, not both: an override of this method replaces the call toonReload()unless it callssuper.onReload(report).- Parameters:
report- the report for this reload, to record the parts that did not reload- Since:
- 6.3.0
-
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); a value that step refuses and keeps, or a part of it that fails, is recorded in theReloadReportas a part that did not reload (#595).finaland always runs its own steps, then callsonReload()-- a module can no longer skip any of this by overridingreloadSelf()itself, because that override point no longer exists (D-01). The hook it calls isonReload(ReloadReport), whose default body callsonReload().Only when every step and the hook returned does it log one framework-owned line naming this module (D-03): an INFO success line, or a WARNING naming the parts that did not reload when the hook recorded any in its
ReloadReport(#529). If anything throws, it logs one SEVERE line naming the module and the cause instead, never the success line, and rethrows the failure unchanged, so a caller can report it --/ul reload <name>replies failure, and a full/ul reloadcarries on with the next module and names this one in its summary (#509).The signature is unchanged since 6.2;
reloadWithReport()runs the same reload and returns the report.- Specified by:
reloadSelfin interfaceIPlugin
-
reloadWithReport
Reloads this module exactly asreloadSelf()does, and returns what its reload hook reported (#529)./ul reloadand/ul reload <name>call this to tell the operator which parts of a module did not reload. A module's own reload command can call it for the same reason.- Returns:
- the report of this reload, never
null; empty when every part reloaded - Throws:
RuntimeException- whatever a reload step or the module's hook threw, after it was logged (a checked exception the hook threw without declaring it propagates unchanged in the same way)Error- whatever a reload step or the module's hook threw, after it was logged- Since:
- 6.3.0
-
performReload
The body ofreloadSelf()andreloadWithReport(). Private, so both public entry points always run exactly this.- Returns:
- the report the module's hook filled in; when the call was refused off the server thread (#538), a report whose only partial reason is that refusal
-
runReloadSteps
The reload steps, in order, ending with the module's own hook.- Parameters:
report- the report handed to the module's hook
-
pendingLanguageSetting
The framework'slanguagesetting as it is inconfig.ymlon disk now, when it differs from the value the framework runs with (#502).The framework reads its configuration once and re-reads it only on a full
/ul reload. A missing, unreadable or unparseable file, or one without the key, is not a pending change: that is reported, if at all, by the full reload that reads it.- Returns:
- the value on disk, or
nullif it is the running value or cannot be read
-
describeFailure
A failure's message for a one-line report, or its class name when it carries none.- Parameters:
failure- the failure to describe- Returns:
- a non-null description
-
getLogger
- Returns:
- plugin logger
-