Class PluginInstallUtils

java.lang.Object
com.ultikits.ultitools.utils.PluginInstallUtils

public class PluginInstallUtils extends Object
Utility class for plugin installation and management operations. Provides methods to download, install, and uninstall UltiTools plugins from the remote plugin repository.
Since:
6.0.0
Author:
wisdomme
  • Field Details

    • LOGGER

      private static final Logger LOGGER
    • DEFAULT_MODULE_CODE_SOURCE

      static final Function<UltiToolsPlugin,File> DEFAULT_MODULE_CODE_SOURCE
      How the uninstall asks a loaded module which JAR it came from; a test passes its own.
    • CATALOGUE

      static final ModuleFileTransactions.Catalogue CATALOGUE
      The UltiCloud catalogue, as the update transaction asks it. A 409 from the level-filtered latest -- versions exist, none this framework installs -- is thrown as ModuleFileTransactions.NoCompatibleVersionException (plan 18-40).
    • GSON

      private static final com.google.gson.Gson GSON
    • baseUrl

      private static volatile String baseUrl
    • customBaseUrl

      private static String customBaseUrl
    • HTTP_CONFLICT

      private static final int HTTP_CONFLICT
      The HTTP status with which the catalogue answers that nothing this framework installs is offered.
      See Also:
    • HTTP_NOT_FOUND

      private static final int HTTP_NOT_FOUND
      The HTTP status with which the catalogue answers that it does not know a module, or knows it but has no finalized version of it.
      See Also:
    • PART_SUFFIX

      static final String PART_SUFFIX
      The suffix of a download not yet checked; the module loader reads only *.jar.
      See Also:
  • Constructor Details

    • PluginInstallUtils

      public PluginInstallUtils()
  • Method Details

    • getBaseUrl

      static String getBaseUrl()
      Get the base URL for API calls. Uses lazy initialization to avoid requiring UltiTools at class loading time.
      Returns:
      the base URL
    • setBaseUrlForTesting

      static void setBaseUrlForTesting(String url)
      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

      static String normalizeIdentifyString(String idString)
    • encodeQueryValue

      private static String encodeQueryValue(String value)
    • encodePathSegment

      private static String encodePathSegment(String value)
    • catalogueUrl

      static String catalogueUrl(String path, int frameworkLevel)
      The URL of a catalogue request: path, with its own query if it has one, under the base URL, followed by apiLevel (this framework's level) and minApiLevel (ModuleApiLevels.MIN_INSTALLABLE_API_LEVEL). Every catalogue request /upm makes is built here, so none can omit them; the catalogue filters latest, versions and the download routes by them and ignores them elsewhere (plan 18-40).
      Parameters:
      path - the route, starting with /, optionally with a query
      frameworkLevel - this framework's API level
      Returns:
      the full URL
    • catalogueUrl

      private static String catalogueUrl(String path)
    • catalogueString

      private static PluginInstallUtils.CatalogueAnswer catalogueString(String url)
    • installedJarName

      static String installedJarName(String idString, String version)
    • unwrapData

      private static <T> T unwrapData(String body, Class<T> clazz)
      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 body
      clazz - the class to deserialize the data field into
      Returns:
      the deserialized data, or null if absent or response indicates error
    • unwrapData

      private static <T> T unwrapData(String body, Type type)
      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 body
      type - the Type to deserialize the data field into
      Returns:
      the deserialized data, or null if absent or response indicates error
    • unwrapStringData

      private static String unwrapStringData(String body)
      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

      public static List<PluginEntity> getPluginList(int page, int pageSize)
      Get plugin list online.
      Parameters:
      page - the page number
      pageSize - the number of items per page
      Returns:
      the plugin list
    • getPluginVersionDownloadLink

      public static String getPluginVersionDownloadLink(String idString, String version)
      Get plugin download link.
      Parameters:
      idString - the plugin ID
      version - the version
      Returns:
      the plugin download link
    • getPluginVersions

      public static List<String> getPluginVersions(String idString)
      Get plugin versions.
      Parameters:
      idString - the plugin ID
      Returns:
      the plugin version list
    • getPluginLatestVersion

      public static String getPluginLatestVersion(String idString)
      Get plugin latest version.

      Returns null for a blank id, a module the catalogue does not know, no version this framework can install, and every failure (an error status, an unreadable body, an unreachable catalogue); this contract is unchanged as of 6.3.0. The start-up update check uses lookUpPluginLatestVersion(String) instead, which tells those apart.

      Parameters:
      idString - the plugin ID
      Returns:
      the plugin's latest version, or null
    • lookUpPluginLatestVersion

      @Internal public static VersionLookup lookUpPluginLatestVersion(String idString)
      Looks up a module's latest version for this framework's API level and says whether the lookup established anything (Phase 19, D19-032). VersionLookup.Outcome.FOUND carries the version exactly as getPluginLatestVersion(String) returns it. VersionLookup.Outcome.NONE is a confirmed "nothing to offer": a blank id, the catalogue not knowing the module (plugin/get answers 404), or knowing it with no version this framework can install or no finalized version (latest answers 409 or 404). Every other answer -- another non-2xx status, an error envelope, a missing data, an unreadable body, an unreachable catalogue -- is VersionLookup.Outcome.FAILED.

      An exception thrown while asking propagates, exactly as it does from getPluginLatestVersion(String).

      Parameters:
      idString - the module's identify string
      Returns:
      the lookup's outcome, never null
      Since:
      6.3.0
    • lookUpPluginLatestVersion

      @Internal public static VersionLookup lookUpPluginLatestVersion(String idString, BooleanSupplier stopped)
      lookUpPluginLatestVersion(String), with a stop signal read between its two requests (Phase 19, D19-032): when stopped is true once the module is found, the latest request is not sent and the lookup is VersionLookup.Outcome.FAILED with the failure "stopped". The start-up update check passes its own stop flag, so a check stopped by the plugin's disable sends no further request.
      Parameters:
      idString - the module's identify string
      stopped - read once, after the first request answers
      Returns:
      the lookup's outcome, never null
      Since:
      6.3.0
    • lookUpLatestOf

      private static VersionLookup lookUpLatestOf(PluginEntity plugin, int frameworkLevel)
      The latest half of lookUpPluginLatestVersion(String), for a module the catalogue knows.
    • getPluginLatestDownloadLink

      public static String getPluginLatestDownloadLink(String idString)
      Get plugin latest version download link.
      Parameters:
      idString - Plugin identify string
      Returns:
      Download link for latest version
    • getPlugin

      public static PluginEntity getPlugin(String idString)
      Get plugin information by identify string.
      Parameters:
      idString - Plugin identify string
      Returns:
      Plugin entity or null if not found
    • getPlugin

      private static PluginEntity getPlugin(String idString, int frameworkLevel)
    • installLatestPlugin

      public static boolean installLatestPlugin(String idString)
      Install latest plugin.

      As of 6.3.0 only a module whose plugin.yml declares an api-version from ModuleApiLevels.MIN_INSTALLABLE_API_LEVEL up to this framework's level is installed; for any other this returns false and writes nothing to the modules folder. See installReporting(String, String).

      Parameters:
      idString - the plugin ID
      Returns:
      whether the install succeeded
    • installPlugin

      public static boolean installPlugin(String idString, String version)
      Install plugin.

      As of 6.3.0 only a module whose plugin.yml declares an api-version from ModuleApiLevels.MIN_INSTALLABLE_API_LEVEL up to this framework's level is installed; for any other this returns false and writes nothing to the modules folder. See installReporting(String, String).

      Parameters:
      idString - the plugin ID
      version - the version
      Returns:
      whether the install succeeded
    • installReporting

      @Internal public static PluginInstallUtils.InstallResult installReporting(String idString, String versionOrNull)
      Installs a module from the catalogue and says why when it does not (plan 18-40).

      Every catalogue request carries this framework's API level (catalogueUrl(java.lang.String, int)). The JAR is downloaded into the modules folder as <final name>.part, a name the module loader never lists or puts on the class path (it reads only *.jar), on the same file system so that accepting it is one atomic rename. Its plugin.yml is then read again, and the module is refused -- the download deleted, the modules folder as it was -- unless its api-version is an integer from ModuleApiLevels.MIN_INSTALLABLE_API_LEVEL to this framework's level. That check is the backstop for a catalogue that does not filter (one deployed before the filter, a self-hosted api-url) or a version recorded wrongly. An accepted JAR replaces a file of the same final name, as the download always did.

      Parameters:
      idString - the module's identify-string
      versionOrNull - the version, or null for the latest version the catalogue offers this framework
      Returns:
      the outcome, carrying an i18n reason key and its arguments when the API level is the reason; a failure without a key is reported as before
    • installVersion

      private static PluginInstallUtils.InstallResult installVersion(PluginEntity plugin, String idString, String requested, String version, int level)
      The second half of installReporting(java.lang.String, java.lang.String): the version is known; fetch its link and install it.
    • downloadAndAdmit

      private static PluginInstallUtils.InstallResult downloadAndAdmit(String link, String fileName, String requested, String version, int level)
      Downloads to <fileName>.part, checks it, and either renames it to fileName or deletes it. The .part file never survives this method, whatever happens.
    • refusalOf

      private static PluginInstallUtils.InstallResult refusalOf(File part, String requested, String version, int level)
      Why a downloaded JAR is not installed, or null to install it: the archive guard the module loader applies (SecurityPolicy.isSafeFileStructure(long, int); the .jar suffix check of SecurityPolicy.isValidModuleJar(java.io.File) does not apply to the .part name), then the declared api-version.
    • isSafeArchive

      private static boolean isSafeArchive(File file)
    • moveIntoPlace

      private static void moveIntoPlace(File part, File target) throws IOException
      Renames the checked download to its final name in one step, replacing a file of that name.
      Throws:
      IOException
    • deleteQuietly

      private static void deleteQuietly(File file)
    • findPluginJar

      public static File findPluginJar(File pluginsFolder, String identifyString)
      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 scan
      identifyString - the identify-string to match
      Returns:
      the matching JAR file, or null if not found
    • stageUpdate

      @Internal public static ModuleFileTransactions.StageResult stageUpdate(String identifyString)
      Stages an update of a loaded module to its latest version (#505).

      Nothing in the modules folder changes: the new JAR is downloaded into the transaction folder beside it and recorded. At the next start the files are swapped before any module loads, and the update is kept only if the module is then seen loaded from the new JAR at the new version; otherwise the old JAR is restored. See ModuleFileTransactions.

      Parameters:
      identifyString - the module's identify-string
      Returns:
      what was staged, or why nothing was
    • recordedMainClass

      static Function<UltiToolsPlugin,String> recordedMainClass(ModuleJarIndex index)
      The main class the module loader recorded for a loaded module, read from its start-up index (#516): what staging checks other copies of the module against (maintainer follow-up 19). null for a module the index has no record of, so that staging falls back to the old JAR's own plugin.yml main: rather than to a runtime class name no JAR declares.
      Parameters:
      index - the loader's index, or null
      Returns:
      the resolver
    • forgetDeferredRemoval

      private static void forgetDeferredRemoval(String fileName)
      After an install wrote fileName into the modules folder, forgets a deletion an earlier uninstall recorded for that name, so the next start does not delete the new install (#518). A failure is logged: the install itself succeeded.
    • updatePlugin

      public static boolean updatePlugin(String identifyString)
      Stages an update of a loaded module; see stageUpdate(String).

      As of 6.3.0 this no longer replaces the JAR in place and no longer means "updated": it returns true when the update was staged, to take effect at the next start and to be kept only if the module then loads.

      Parameters:
      identifyString - the plugin identify string
      Returns:
      true if the update was staged
    • probe

      private static PluginInstallUtils.Presence probe(File file)
      What is at file, without following links, distinguishing "not there" from "could not be determined".
      Parameters:
      file - the path to probe
      Returns:
      what is there
    • probeFollowingLinks

      private static PluginInstallUtils.Presence probeFollowingLinks(File file)
      What is at file with 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#init opens each entry through new JarFile(file), which follows links, so a link to a real JAR is a file it loads from. The access question keeps NOFOLLOW_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

      private static PluginInstallUtils.Presence probe(File file, LinkOption... options)
      probe(File) with the link options to use.
      Parameters:
      file - the path to probe
      options - how to treat a symbolic link
      Returns:
      what is there
    • codeSourceJarOf

      static File codeSourceJarOf(Class<?> moduleClass)
      The JAR a module class was loaded from, or null when it was not loaded from one.

      The loaded instance is the authority on which file it came from, whatever that file's plugin.yml says now: the file may have been replaced or edited since the start, and a module registered from code has no plugin.yml entry the loader read at all. So the framework asks the instance, exactly as UltiToolsPlugin itself 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 null when there is none to name
    • codeSourceLocationOf

      static File codeSourceLocationOf(Class<?> moduleClass)
      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 null is "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 null when it could not be determined
    • resolveCodeSourceFile

      private static File resolveCodeSourceFile(URL location)
      A code source URL as a file, whatever escaping it carries.
    • readArchive

      private static PluginInstallUtils.ArchiveIdentity readArchive(File file)
      Reads what an archive declares about itself.

      The one place a plugin.yml is opened. Four outcomes, each a positive answer: it declares a name; it opened and declares none (no plugin.yml, or one without a name: 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
      The name a plugin.yml declares, if it declares one.
      Parameters:
      jarFile - the archive
      entry - its plugin.yml
      file - the archive's path, for the log line
      Returns:
      what it says about itself
      Throws:
      IOException - when the entry cannot be read
    • canonicalPathOf

      private static String canonicalPathOf(File file)
      file's canonical path, or its absolute path when it cannot be canonicalised.
    • carrying

      private static <T extends Throwable> T carrying(T failure, List<String> entries)
      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 uninstall
      entries - the absolute paths of the entries nothing could identify
      Returns:
      failure
    • uninstallPlugin

      public static boolean uninstallPlugin(String name) throws IOException
      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). Calling plugin.unregisterSelf() directly, as this method used to, skipped everything unregister does first -- cancelling the module's @Scheduled tasks, releasing its @PlayerCache beans, 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 returned true whether 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 one PluginInstallUtils.EntryState, and what the method returns or throws says which of those it met.

      As of 6.3.0 an entry is this module's also when its plugin.yml main: names the module's main class, the entry the module loader loads by (#516), and an uninstall that goes ahead also cancels any update of the module waiting for the next start (uninstallPluginReporting(String, List)).

      Parameters:
      name - the module's runtime name -- which need not be what its JAR's plugin.yml declares, since UltiToolsPlugin(String pluginName, ...) takes it as an argument and reads no metadata
      Returns:
      true when every entry identified as this module's was deleted; false when 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 holds
      FileSystemException - when an entry identified as this module's could not be deleted, naming every one still on disk: every FileSystemException in the failure, including those attached as suppressed, names a module JAR still on disk. When the deletion could not be recorded for the next start either, that failure travels attached as a PluginInstallUtils.RemovalNotRecordedException, which is not a FileSystemException
      NoSuchFileException - when a loaded module was unloaded but nothing in the folder could be identified as its JAR, which is not a spelling mistake
      IllegalStateException - when the module's own unload threw. The module is still removed from the loaded modules and its JARs are still deleted: unregister has 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 as uninstallPlugin(String) documents
    • uninstallPluginReporting

      @Internal public static PluginInstallUtils.UninstallReport uninstallPluginReporting(String name, List<String> cancelledUpdates) throws IOException
      uninstallPluginReporting(String), also collecting the versions of the module's updates it cancelled.

      An uninstall that goes ahead -- whatever happens to its JARs afterwards, including a modules folder that cannot be listed -- cancels every update of the module still waiting for the next start and any download of one still running, so the next start does not install the module again (#505). It matches them on the identity it resolved before touching the folder: the identify-strings and runtime names of the instances it unloaded and their JARs, never only the name it was given (round-10 review). A refused uninstall changed nothing and cancels nothing.

      Parameters:
      name - the module's runtime name, or a name a JAR of it declares
      cancelledUpdates - collects the version of each staged update cancelled; filled on every outcome, including a thrown one, except a refusal
      Returns:
      what the uninstall did and what it could not determine
      Throws:
      IOException - exactly as uninstallPlugin(String) documents
    • uninstallPluginReporting

      @Internal static PluginInstallUtils.UninstallReport uninstallPluginReporting(String name, Function<UltiToolsPlugin,File> codeSource) throws IOException
      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).
      Parameters:
      name - the module's runtime name, or a name a JAR of it declares
      codeSource - 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 as uninstallPlugin(String) documents
    • uninstallPluginReporting

      static PluginInstallUtils.UninstallReport uninstallPluginReporting(String name, Function<UltiToolsPlugin,File> codeSource, List<String> cancelledUpdates) throws IOException
      uninstallPluginReporting(String, List) with the code-source resolver to ask.
      Parameters:
      name - the module's runtime name, or a name a JAR of it declares
      codeSource - how to ask a loaded module which JAR it came from
      cancelledUpdates - collects the version of each staged update cancelled
      Returns:
      what the uninstall did and what it could not determine
      Throws:
      IOException - exactly as uninstallPlugin(String) documents
    • removeJarsAndReport

      private static PluginInstallUtils.UninstallReport removeJarsAndReport(String name, PluginInstallUtils.ModuleIdentity identity, Throwable unloadFailure) throws IOException
      The JAR half of the uninstall and its report, after the modules were unloaded.
      Parameters:
      name - the operator's argument
      identity - the resolved identity
      unloadFailure - what unloading threw, or null
      Returns:
      the report
      Throws:
      IOException - as documented on uninstallPlugin(String)
    • cancelUpdatesOf

      private static void cancelUpdatesOf(PluginInstallUtils.ModuleIdentity identity, List<String> cancelledUpdates)
      Cancels the uninstalled module's staged updates and running update downloads, matched on the identity this uninstall resolved (round-10 review). Never throws: it runs on every exit of the uninstall, and a failure here must not replace the outcome being reported.
      Parameters:
      identity - the resolved identity
      cancelledUpdates - collects the versions cancelled
    • chooseTargets

      private static void chooseTargets(String requested, List<UltiToolsPlugin> loaded, List<UltiToolsPlugin> others, Map<UltiToolsPlugin,String> declared)
      Moves the instances the argument names from others into loaded.

      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 argument
      loaded - collects the instances it names
      others - every loaded instance; the chosen ones are moved out of it
      declared - what each instance's JAR declares
    • refuseIfAmbiguous

      private static void refuseIfAmbiguous(String requested, List<UltiToolsPlugin> candidates)
      Refuses when more than one module answers to a declared name.
      Parameters:
      requested - the operator's argument
      candidates - 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 argument
      pluginManager - the manager that owns the plugin list
      codeSource - how to ask a loaded module which JAR it came from
      Returns:
      the resolved identity
    • recordedOf

      private static PluginInstallUtils.Recorded recordedOf(PluginManager pluginManager, List<UltiToolsPlugin> targets, List<UltiToolsPlugin> others, Set<String> ownJars)
      What the loader recorded about the targets (#516) and what they report about themselves, read before they are unloaded.
      Parameters:
      pluginManager - the manager whose start-up scan recorded them
      targets - the loaded instances this uninstall targets
      others - every other loaded instance
      ownJars - the canonical paths of the targets' code-source JARs
      Returns:
      the record
    • addNormalized

      private static void addNormalized(Set<String> into, String identifyString)
      Adds identifyString, normalised, unless it is absent or blank.
    • mainClassesOf

      private static Set<String> mainClassesOf(PluginManager pluginManager, List<UltiToolsPlugin> targets, List<UltiToolsPlugin> others)
      The main classes the module loader recorded for the targets, less every one another loaded module also has (#516) -- the same exclusion the name keys get (gate 1, BL-02).
      Parameters:
      pluginManager - the manager whose start-up scan recorded them
      targets - the loaded instances this uninstall targets
      others - every other loaded instance
      Returns:
      the main classes an entry is matched on
    • 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 list
      codeSource - how to ask a loaded module which JAR it came from
      others - collects every loaded instance, before any is chosen as a target
      jars - collects each instance's code-source JAR
      declared - collects what each instance's JAR declares
      someUnknown - 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, which also 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 unloaded
      pluginManager - the manager that owns the plugin list
      Returns:
      the first unload failure with any later one attached, or null
    • firstOf

      private static Throwable firstOf(Throwable first, Throwable next)
      The first failure, with any later one attached to it.

      Combining two failures has more ways to fail than it looks. addSuppressed rejects a throwable suppressing itself, which a module reusing a static sentinel produces from two instances -- and that IllegalArgumentException would escape past every handler, abandoning the uninstall with the modules delisted and their JARs still on disk. It also rejects null. Attaching the same object twice is not an error but says nothing, so it is skipped too.

      Parameters:
      first - the failure collected so far, or null
      next - the failure to attach
      Returns:
      the failure to carry on with
    • alreadyAttached

      private static boolean alreadyAttached(Throwable failure, Throwable candidate)
      Whether candidate is already among failure's suppressed throwables.
    • deleteModuleJars

      private static PluginInstallUtils.UninstallReport deleteModuleJars(PluginInstallUtils.ModuleIdentity identity) throws IOException
      The JAR half of uninstallPlugin(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 on uninstallPlugin(String)
    • deferOrFail

      private static FileSystemException deferOrFail(PluginInstallUtils.ModuleIdentity identity, List<File> jars, FileSystemException deleteFailure)
      A JAR that could not be deleted now is recorded so the next start deletes it before any module loads (#518). On Windows the shared module class loader keeps every module JAR open while the server runs, so "delete it by hand" is an instruction the operator cannot follow; the record is what makes the uninstall complete. Only when the record itself cannot be written does the original failure leave as it was, with the record failure attached inside a PluginInstallUtils.RemovalNotRecordedException: every FileSystemException in the failure still names a module JAR still on disk, never a record or temporary file under .ultikits.
      Parameters:
      identity - the resolved identity
      jars - the JARs the uninstall tried to delete
      deleteFailure - what the deletion threw
      Returns:
      the failure to report: PluginInstallUtils.RemovalDeferredException when the deletion was recorded; otherwise deleteFailure, with a PluginInstallUtils.RemovalNotRecordedException attached when the record could not be written
    • refuseIfAnUpdateIsHeldForTheOperator

      private static void refuseIfAnUpdateIsHeldForTheOperator(PluginInstallUtils.ModuleIdentity identity)
      Refuses, before anything is unloaded, while an update of the module is held for the operator: its record names a file the transaction could not identify, and the uninstall's cancellation would otherwise have to delete that record's folder, files included (foreign-file rule).
    • refuseIfAnotherModuleSharesTheJar

      private static void refuseIfAnotherModuleSharesTheJar(PluginInstallUtils.ModuleIdentity identity)
    • 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' JARs
      toDelete - the entries about to be deleted
    • modulesFolder

      private static File modulesFolder()
      The modules folder install and uninstall act on: the one PluginManager#init scans and the module class loader reads, from the one method that computes it (#517). Before 6.3.0 the scan read System.getProperty("user.dir") + /plugins/UltiTools/plugins while this read the data folder, and the two diverged on a server started from another directory.
      Returns:
      the modules folder
    • modulesFolderUnlistable

      private static AccessDeniedException modulesFolderUnlistable(File folder)
      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)
      Which PluginInstallUtils.EntryState an 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 no plugin.yml or no name: -- is state C.

      Parameters:
      file - the entry
      identity - the resolved identity, whose keys and own JARs decide this
      Returns:
      the state it is in
    • byDeclaredMainClass

      private static PluginInstallUtils.EntryState byDeclaredMainClass(PluginInstallUtils.ModuleIdentity identity)
      An entry whose plugin.yml declares one of the targets' main classes: the module loader loads exactly the class main: names (#548/#549), so after a restart this file loads the module the operator removed (#516). The same caution as a name match applies: while some loaded module's own JAR could not be determined, the entry may be that module's, and is reported.
      Parameters:
      identity - the resolved identity
      Returns:
      the entry's state
    • absolutePathsOf

      private static List<String> absolutePathsOf(List<File> files)
      The absolute paths of files, in the order given.
    • deleteAllOrThrow

      private static void deleteAllOrThrow(List<File> files) throws FileSystemException
      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 folder
      identity - the resolved identity
      undetermined - 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 name
      unloadFailure - what its unload threw
      jarFailure - the JAR outcome to attach, or null when the JARs were deleted
      Returns:
      the exception to throw