Class PluginInstallUtils
- Since:
- 6.0.0
- Author:
- wisdomme
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic final classRaised when the name the operator typed could be more than one loaded module's, so the uninstall changed nothing.private static final classWhat one archive says about which module it holds -- the single reader both identity resolution and entry classification use, so the two cannot drift apart.private static enumWhat one entry of the modules folder is, relative to the module being uninstalled.private static final classEverything later decisions need about which module this is, resolved once.static final classRaised when a module's own unload threw.private static enumWhat a filesystem probe answered, keeping "unknown" out of "no".static final classThe entries an uninstall could not identify, carried on whatever failure leaves the method.static classRaised when the uninstall refuses to act because its outcome would be undefined: two possible targets, or a target whose JAR another running module shares.static final classWhat an uninstall did, and what it could not determine. -
Field Summary
Fields -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionabsolutePathsOf(List<File> files) The absolute paths offiles, in the order given.private static booleanalreadyAttached(Throwable failure, Throwable candidate) Whethercandidateis already amongfailure's suppressed throwables.private static StringcanonicalPathOf(File file) file's canonical path, or its absolute path when it cannot be canonicalised.private static <T extends Throwable>
TAttaches the undetermined entries to a failure on its way out, when there are any.private static voidchooseTargets(String requested, List<UltiToolsPlugin> loaded, List<UltiToolsPlugin> others, Map<UltiToolsPlugin, String> declared) Moves the instances the argument names fromothersintoloaded.private static PluginInstallUtils.EntryStateclassify(File file, PluginInstallUtils.ModuleIdentity identity) WhichPluginInstallUtils.EntryStatean entry of the modules folder is in.(package private) static FilecodeSourceJarOf(Class<?> moduleClass) The JAR a module class was loaded from, ornullwhen it was not loaded from one.(package private) static FilecodeSourceLocationOf(Class<?> moduleClass) Whether which JAR a module was loaded from could be determined at all.private static voiddeleteAllOrThrow(List<File> files) Deletes every file, and reports the ones that are still there.private static PluginInstallUtils.UninstallReportThe JAR half ofuninstallPlugin(String): deletes every entry identified as this module's -- not only the first one listed, since a second JAR loads the module again on restart -- and reports the entries nothing could be read from.private static StringencodePathSegment(String value) private static StringencodeQueryValue(String value) static FilefindPluginJar(File pluginsFolder, String identifyString) Find the JAR file for a plugin by its identify-string.private static ThrowableThe first failure, with any later one attached to it.(package private) static StringGet the base URL for API calls.static PluginEntityGet plugin information by identify string.static StringgetPluginLatestDownloadLink(String idString) Get plugin latest version download link.static StringgetPluginLatestVersion(String idString) Get plugin latest version.static List<PluginEntity> getPluginList(int page, int pageSize) Get plugin list online.static StringgetPluginVersionDownloadLink(String idString, String version) Get plugin download link.getPluginVersions(String idString) Get plugin versions.(package private) static StringinstalledJarName(String idString, String version) static booleaninstallLatestPlugin(String idString) Install latest plugin.static booleaninstallPlugin(String idString, String version) Install plugin.private static FileThe folder the uninstall acts on.private static AccessDeniedExceptionmodulesFolderUnlistable(File folder) The state-D failure: the folder is there, and what it holds is unknown.private static booleannoJarFound(File folder, PluginInstallUtils.ModuleIdentity identity, List<String> undetermined) What "nothing here could be identified as this module's" means, which depends on whether one was loaded.private static StringnormalizeIdentifyString(String idString) private static PluginInstallUtils.PresenceWhat is atfile, without following links, distinguishing "not there" from "could not be determined".private static PluginInstallUtils.Presenceprobe(File file, LinkOption... options) probe(File)with the link options to use.private static PluginInstallUtils.PresenceprobeFollowingLinks(File file) What is atfilewith links followed: the question "is this one of the files the module loader would open?", which is a different question from "is anything at this path?".private static PluginInstallUtils.ArchiveIdentityreadArchive(File file) Reads what an archive declares about itself.private static PluginInstallUtils.ArchiveIdentityreadDeclaredName(JarFile jarFile, JarEntry entry, File file) Thenameaplugin.ymldeclares, if it declares one.private static voidreadLoadedModules(PluginManager pluginManager, Function<UltiToolsPlugin, File> codeSource, List<UltiToolsPlugin> others, Map<UltiToolsPlugin, File> jars, Map<UltiToolsPlugin, String> declared, AtomicBoolean someUnknown) Reads every loaded module once: which JAR it came from, and what that JAR declares.private static voidrefuseIfAmbiguous(String requested, List<UltiToolsPlugin> candidates) Refuses when more than one module answers to a declared name.private static voidRefuses before anything is unloaded when a JAR this uninstall would act on is also the JAR another running module was loaded from.private static voidrefuseToTouchAnotherModulesJar(PluginInstallUtils.ModuleIdentity identity, List<File> toDelete) The invariant, checked immediately before anything is deleted: no JAR another loaded module was loaded from may be removed, whatever decided to get here.(package private) static voidReset the base URL to force re-initialization.private static FileresolveCodeSourceFile(URL location) A code source URL as a file, whatever escaping it carries.private static PluginInstallUtils.ModuleIdentityresolveIdentity(String requested, PluginManager pluginManager, Function<UltiToolsPlugin, File> codeSource) Resolves the operator's argument into the one identity every later decision reads.(package private) static voidSet a custom base URL for testing purposes.static booleanuninstallPlugin(String name) Uninstalls a module: unloads every loaded instance of it and deletes its JARs.uninstallPlugin(String), also reporting the entries whose identity could not be determined -- the ones a caller has to tell the operator about, because one of them may be a copy of this module that loads again at the next start.(package private) static PluginInstallUtils.UninstallReportuninstallPluginReporting(String name, Function<UltiToolsPlugin, File> codeSource) uninstallPluginReporting(String)with the code-source resolver to ask, which is how a test substitutes one without production code carrying a writable field (gate 1, IN-02).private static ThrowableunloadEvery(PluginInstallUtils.ModuleIdentity identity, PluginManager pluginManager) Unloads every matching module through the framework's full unload path and delists it.private static PluginInstallUtils.ModuleUnloadFailedExceptionunloadFailed(String name, Throwable unloadFailure, IOException jarFailure) The failure raised when a module's own unload threw, after the uninstall went ahead anyway.private static <T> TunwrapData(String body, Class<T> clazz) Extract the "data" field from a backend API envelope response.private static <T> TunwrapData(String body, Type type) Extract the "data" field from a backend API envelope response (generic type version).private static StringunwrapStringData(String body) Extract a String value from the "data" field of a backend API envelope response.static booleanupdatePlugin(String identifyString) Update a plugin module: download latest version and delete old JAR.
-
Field Details
-
LOGGER
-
DEFAULT_MODULE_CODE_SOURCE
How the uninstall asks a loaded module which JAR it came from; a test passes its own. -
GSON
private static final com.google.gson.Gson GSON -
baseUrl
-
customBaseUrl
-
-
Constructor Details
-
PluginInstallUtils
public PluginInstallUtils()
-
-
Method Details
-
getBaseUrl
Get the base URL for API calls. Uses lazy initialization to avoid requiring UltiTools at class loading time.- Returns:
- the base URL
-
setBaseUrlForTesting
Set a custom base URL for testing purposes. This should only be used in unit tests.- Parameters:
url- the custom base URL, or null to reset to default
-
resetBaseUrl
static void resetBaseUrl()Reset the base URL to force re-initialization. This should only be used in unit tests. -
normalizeIdentifyString
-
encodeQueryValue
-
encodePathSegment
-
installedJarName
-
unwrapData
Extract the "data" field from a backend API envelope response. The backend wraps responses as {"code":"200","msg":"Success","data":...}.- Type Parameters:
T- the target type- Parameters:
body- the raw JSON response bodyclazz- the class to deserialize the data field into- Returns:
- the deserialized data, or null if absent or response indicates error
-
unwrapData
Extract the "data" field from a backend API envelope response (generic type version).- Type Parameters:
T- the target type- Parameters:
body- the raw JSON response bodytype- the Type to deserialize the data field into- Returns:
- the deserialized data, or null if absent or response indicates error
-
unwrapStringData
Extract a String value from the "data" field of a backend API envelope response. Handles both quoted JSON strings and primitive values.- Parameters:
body- the raw JSON response body- Returns:
- the data as a String, or null if absent or response indicates error
-
getPluginList
Get plugin list online.- Parameters:
page- the page numberpageSize- the number of items per page- Returns:
- the plugin list
-
getPluginVersionDownloadLink
Get plugin download link.- Parameters:
idString- the plugin IDversion- the version- Returns:
- the plugin download link
-
getPluginVersions
Get plugin versions.- Parameters:
idString- the plugin ID- Returns:
- the plugin version list
-
getPluginLatestVersion
Get plugin latest version.- Parameters:
idString- the plugin ID- Returns:
- the plugin's latest version
-
getPluginLatestDownloadLink
Get plugin latest version download link.- Parameters:
idString- Plugin identify string- Returns:
- Download link for latest version
-
getPlugin
Get plugin information by identify string.- Parameters:
idString- Plugin identify string- Returns:
- Plugin entity or null if not found
-
installLatestPlugin
Install latest plugin.- Parameters:
idString- the plugin ID- Returns:
- whether the install succeeded
-
installPlugin
Install plugin.- Parameters:
idString- the plugin IDversion- the version- Returns:
- whether the install succeeded
-
findPluginJar
Find the JAR file for a plugin by its identify-string. Scans all JARs in the given directory, reads plugin.yml from each.- Parameters:
pluginsFolder- the folder to scanidentifyString- the identify-string to match- Returns:
- the matching JAR file, or null if not found
-
updatePlugin
Update a plugin module: download latest version and delete old JAR.- Parameters:
identifyString- the plugin identify string- Returns:
- true if update succeeded
-
probe
What is atfile, without following links, distinguishing "not there" from "could not be determined".- Parameters:
file- the path to probe- Returns:
- what is there
-
probeFollowingLinks
What is atfilewith links followed: the question "is this one of the files the module loader would open?", which is a different question from "is anything at this path?".PluginManager#initopens each entry throughnew JarFile(file), which follows links, so a link to a real JAR is a file it loads from. The access question keepsNOFOLLOW_LINKS, because that is what separates absence from a denial; this one must match the loader.- Parameters:
file- the path to probe- Returns:
- what is at the end of it
-
probe
probe(File)with the link options to use.- Parameters:
file- the path to probeoptions- how to treat a symbolic link- Returns:
- what is there
-
codeSourceJarOf
The JAR a module class was loaded from, ornullwhen it was not loaded from one.A module that is loaded does not have to be identified by metadata at all: UltiTools modules are recognised by
@UltiToolsModule, andPluginManagerselects a module class withUltiToolsPlugin.class.isAssignableFromand instantiates it throughgetDeclaredConstructor().newInstance()-- noplugin.ymlis consulted. So the framework asks the instance instead, exactly asUltiToolsPluginitself does when it reads its own embedded resources.A code source that is a directory is a development checkout rather than an installed JAR and answers nothing about which file to delete. One that cannot be probed is still named: the module was loaded from it, and a probe that cannot answer is not an answer of "no".
- Parameters:
moduleClass- the loaded module's class- Returns:
- the JAR, or
nullwhen there is none to name
-
codeSourceLocationOf
Whether which JAR a module was loaded from could be determined at all.Three answers through one value, because the difference matters: a file is the JAR the module came from; a directory is an exploded development checkout, which is no JAR at all and says so; and
nullis "could not be determined" -- a code source a policy refuses to reveal, or a class that has none. Unknown is not "none": while it holds, any entry matched only by a name might be that running module's own JAR.- Parameters:
moduleClass- the loaded module's class- Returns:
- the code source's location, or
nullwhen it could not be determined
-
resolveCodeSourceFile
A code source URL as a file, whatever escaping it carries. -
readArchive
Reads what an archive declares about itself.The one place a
plugin.ymlis opened. Four outcomes, each a positive answer: it declares a name; it opened and declares none (noplugin.yml, or one without aname:key -- which say the same thing, since a module needs no metadata at all); it could not be opened or parsed; or the entry is one the module loader itself would never load.- Parameters:
file- the entry- Returns:
- what it says about itself
-
readDeclaredName
private static PluginInstallUtils.ArchiveIdentity readDeclaredName(JarFile jarFile, JarEntry entry, File file) throws IOException Thenameaplugin.ymldeclares, if it declares one.- Parameters:
jarFile- the archiveentry- itsplugin.ymlfile- the archive's path, for the log line- Returns:
- what it says about itself
- Throws:
IOException- when the entry cannot be read
-
canonicalPathOf
file's canonical path, or its absolute path when it cannot be canonicalised. -
carrying
Attaches the undetermined entries to a failure on its way out, when there are any.- Type Parameters:
T- the failure's type- Parameters:
failure- the failure leaving the uninstallentries- the absolute paths of the entries nothing could identify- Returns:
failure
-
uninstallPlugin
Uninstalls a module: unloads every loaded instance of it and deletes its JARs.Unloading goes through
PluginManager.unregister(UltiToolsPlugin), the framework's one full unload path (#503). Callingplugin.unregisterSelf()directly, as this method used to, skipped everythingunregisterdoes first -- cancelling the module's@Scheduledtasks, releasing its@PlayerCachebeans, its tab-completion completers, its EventBus handlers and its conditional-bean records, then closing its context. A module "uninstalled" that way kept running its repeating tasks until the next restart.What it reports is what happened on disk (#501). The old implementation ignored
File.delete()'s result and returnedtruewhether or not the JAR was removed, and it stopped at the first matching JAR, so a second JAR of the same module loaded it again on restart. Now every entry the modules folder holds is placed in exactly onePluginInstallUtils.EntryState, and what the method returns or throws says which of those it met.- Parameters:
name- the module's runtime name -- which need not be what its JAR'splugin.ymldeclares, sinceUltiToolsPlugin(String pluginName, ...)takes it as an argument and reads no metadata- Returns:
truewhen every entry identified as this module's was deleted;falsewhen none was and no loaded module of that name was unloaded either -- the "check the spelling" case- Throws:
AccessDeniedException- when the modules folder exists but could not be listed, so nothing can be concluded about what it holdsFileSystemException- when an entry identified as this module's could not be deleted, naming every one still on diskNoSuchFileException- when a loaded module was unloaded but nothing in the folder could be identified as its JAR, which is not a spelling mistakeIllegalStateException- when the module's own unload threw. The module is still removed from the loaded modules and its JARs are still deleted:unregisterhas closed its context by then, and keeping the JAR would bring back on restart a module the operator asked to remove. A JAR failure is attached as suppressed.IOException- if the modules folder cannot be read
-
uninstallPluginReporting
@Internal public static PluginInstallUtils.UninstallReport uninstallPluginReporting(String name) throws IOException uninstallPlugin(String), also reporting the entries whose identity could not be determined -- the ones a caller has to tell the operator about, because one of them may be a copy of this module that loads again at the next start.- Parameters:
name- the module's runtime name- Returns:
- what the uninstall did and what it could not determine
- Throws:
IOException- exactly asuninstallPlugin(String)documents
-
uninstallPluginReporting
@Internal static PluginInstallUtils.UninstallReport uninstallPluginReporting(String name, Function<UltiToolsPlugin, File> codeSource) throws IOExceptionuninstallPluginReporting(String)with the code-source resolver to ask, which is how a test substitutes one without production code carrying a writable field (gate 1, IN-02).- Parameters:
name- the module's runtime name, or a name a JAR of it declarescodeSource- how to ask a loaded module which JAR it came from- Returns:
- what the uninstall did and what it could not determine
- Throws:
IOException- exactly asuninstallPlugin(String)documents
-
chooseTargets
private static void chooseTargets(String requested, List<UltiToolsPlugin> loaded, List<UltiToolsPlugin> others, Map<UltiToolsPlugin, String> declared) Moves the instances the argument names fromothersintoloaded.The two namespaces are asked in order, never together. A runtime-name match answers the question outright: if any loaded module answers to the name that was typed, that is the answer and no declared-name lookup runs beside it -- asking both at once made one module's runtime name and another's declared name equally good, and unloaded both.
Only when nothing answers by runtime name is the other namespace consulted, because naming a module by what its JAR declares must still work (gate 1, BL-01). If more than one module answers that way, the uninstall refuses and names the candidates rather than choosing: this deletes files, and a destructive command with two possible targets must stop.
- Parameters:
requested- the operator's argumentloaded- collects the instances it namesothers- every loaded instance; the chosen ones are moved out of itdeclared- what each instance's JAR declares
-
refuseIfAmbiguous
Refuses when more than one module answers to a declared name.- Parameters:
requested- the operator's argumentcandidates- the loaded modules whose JARs declare it
-
resolveIdentity
private static PluginInstallUtils.ModuleIdentity resolveIdentity(String requested, PluginManager pluginManager, Function<UltiToolsPlugin, File> codeSource) Resolves the operator's argument into the one identity every later decision reads.Argument to loaded instances: an instance matches when its runtime name is the argument, or when the JAR it was loaded from declares that name -- the second half is gate 1's BL-01, where naming a module by what its JAR declares deleted the JAR of a module that was left running.
Instances to keys: what their own JARs declare, plus the argument. A key another loaded module owns -- its runtime name, or what its own JAR declares -- is dropped, which is gate 1's BL-02: a name lifted off one module's JAR is not safe as a global key. Those modules' JARs are also remembered, because no decision may end in deleting one of them.
With nothing loaded, the argument is the only key there is, and that is recorded.
- Parameters:
requested- the operator's argumentpluginManager- the manager that owns the plugin listcodeSource- how to ask a loaded module which JAR it came from- Returns:
- the resolved identity
-
readLoadedModules
private static void readLoadedModules(PluginManager pluginManager, Function<UltiToolsPlugin, File> codeSource, List<UltiToolsPlugin> others, Map<UltiToolsPlugin, File> jars, Map<UltiToolsPlugin, String> declared, AtomicBoolean someUnknown) Reads every loaded module once: which JAR it came from, and what that JAR declares.Read before anything is unloaded: afterwards the instance and its class loader may be gone, and with them the only authoritative answer to "which JAR is this module's". Which of them the argument names is decided afterwards, by
chooseTargets(java.lang.String, java.util.List<com.ultikits.ultitools.abstracts.UltiToolsPlugin>, java.util.List<com.ultikits.ultitools.abstracts.UltiToolsPlugin>, java.util.Map<com.ultikits.ultitools.abstracts.UltiToolsPlugin, java.lang.String>).- Parameters:
pluginManager- the manager that owns the plugin listcodeSource- how to ask a loaded module which JAR it came fromothers- collects every loaded instance, before any is chosen as a targetjars- collects each instance's code-source JARdeclared- collects what each instance's JAR declaressomeUnknown- set when some instance's code source could not be determined at all
-
unloadEvery
private static Throwable unloadEvery(PluginInstallUtils.ModuleIdentity identity, PluginManager pluginManager) Unloads every matching module through the framework's full unload path and delists it.A module that throws while unloading is collected rather than propagated: it is already unloaded and its context already closed, so the rest of the uninstall goes ahead and the failure is reported together with the JAR outcome. It leaves the plugin list either way, because a listed module whose context is closed would be reported as still loaded.
- Parameters:
identity- the resolved identity, whose loaded instances are unloadedpluginManager- the manager that owns the plugin list- Returns:
- the first unload failure with any later one attached, or
null
-
firstOf
The first failure, with any later one attached to it.Combining two failures has more ways to fail than it looks.
addSuppressedrejects a throwable suppressing itself, which a module reusing a static sentinel produces from two instances -- and thatIllegalArgumentExceptionwould escape past every handler, abandoning the uninstall with the modules delisted and their JARs still on disk. It also rejectsnull. Attaching the same object twice is not an error but says nothing, so it is skipped too.- Parameters:
first- the failure collected so far, ornullnext- the failure to attach- Returns:
- the failure to carry on with
-
alreadyAttached
Whethercandidateis already amongfailure's suppressed throwables. -
deleteModuleJars
private static PluginInstallUtils.UninstallReport deleteModuleJars(PluginInstallUtils.ModuleIdentity identity) throws IOException The JAR half ofuninstallPlugin(String): deletes every entry identified as this module's -- not only the first one listed, since a second JAR loads the module again on restart -- and reports the entries nothing could be read from.- Parameters:
identity- the resolved identity: which instances were loaded, which JARs they came from, which keys other entries are matched against, and which JARs belong to other loaded modules- Returns:
- what was deleted and what could not be identified
- Throws:
IOException- as documented onuninstallPlugin(String)
-
refuseToTouchAnotherModulesJar
private static void refuseToTouchAnotherModulesJar(PluginInstallUtils.ModuleIdentity identity, List<File> toDelete) The invariant, checked immediately before anything is deleted: no JAR another loaded module was loaded from may be removed, whatever decided to get here.Gate 1's BL-02 was a bystander module losing its JAR because a name lifted off the target's JAR became the match key. Resolution no longer produces such a key, and this is the guard at the point of destruction for every other way it could happen. It fires before the first delete, so nothing is half-done when it does.
- Parameters:
identity- the resolved identity, carrying the other loaded modules' JARstoDelete- the entries about to be deleted
-
modulesFolder
The folder the uninstall acts on.PluginManager#initscansSystem.getProperty("user.dir")+/plugins/UltiTools/pluginswhile this reads the framework's own data folder. On a server started from its own root the two are one path and can diverge when it is not; that they are computed twice is gate 1's IN-05, filed as its own issue. This side uses the data folder because every other file the uninstall touches is relative to it.- Returns:
- the modules folder
-
modulesFolderUnlistable
The state-D failure: the folder is there, and what it holds is unknown. -
classify
private static PluginInstallUtils.EntryState classify(File file, PluginInstallUtils.ModuleIdentity identity) WhichPluginInstallUtils.EntryStatean entry of the modules folder is in.The loaded instance's own code-source JAR is state A whatever it declares -- the module itself said where it came from. Otherwise the archive is read once (
readArchive(java.io.File)): an entry the module loader would never load, and one declaring a module that is not this one, are state B; one that declares a key this identity owns is state A; and one that says nothing about itself -- would not open, cannot be parsed, carries noplugin.ymlor noname:-- is state C.- Parameters:
file- the entryidentity- the resolved identity, whose keys and own JARs decide this- Returns:
- the state it is in
-
absolutePathsOf
The absolute paths offiles, in the order given. -
deleteAllOrThrow
Deletes every file, and reports the ones that are still there.- Parameters:
files- the JARs to delete- Throws:
FileSystemException- naming the first JAR that survived, with any other attached as suppressed -- each one of them loads the module again on restart
-
noJarFound
private static boolean noJarFound(File folder, PluginInstallUtils.ModuleIdentity identity, List<String> undetermined) throws NoSuchFileException What "nothing here could be identified as this module's" means, which depends on whether one was loaded.- Parameters:
folder- the modules folderidentity- the resolved identityundetermined- the entries nothing could identify, attached to the failure- Returns:
false, the "no such module" answer, when nothing was unloaded either- Throws:
NoSuchFileException- when a module was unloaded and its JAR is missing, with one suppressed entry per undetermined file
-
unloadFailed
private static PluginInstallUtils.ModuleUnloadFailedException unloadFailed(String name, Throwable unloadFailure, IOException jarFailure) The failure raised when a module's own unload threw, after the uninstall went ahead anyway.- Parameters:
name- the module's runtime nameunloadFailure- what its unload threwjarFailure- the JAR outcome to attach, ornullwhen the JARs were deleted- Returns:
- the exception to throw
-