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. Versions exist but none this framework installs -- the level-filtered list is empty -- is thrown as ModuleFileTransactions.NoCompatibleVersionException (plan 18-40).
    • baseUrl

      private static volatile String baseUrl
    • customBaseUrl

      private static String customBaseUrl
    • 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)
    • installedJarName

      static String installedJarName(String idString, String version)
    • 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
    • lookUpPluginVersions

      @Internal public static VersionLookup lookUpPluginVersions(String idString, List<String> names)
      Internal command lookup that distinguishes an unknown module from a successful empty list. This bridge is not part of the supported plugin API; getPluginVersions(String) keeps its existing empty-list/null contract.
      Parameters:
      idString - the plugin ID
      names - collects version names on success
      Returns:
      FOUND with the module identifier on success, NONE when the versions endpoint answers 404, or FAILED when the lookup establishes nothing
    • 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
    • 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
    • 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 v2 catalogue and says why when it does not (plan 22-18).

      The version's metadata names the byte count and the SHA-256 of the JAR it offers, and the JAR is accepted only when both match the bytes that arrived (see VerifiedDownload). It is downloaded under 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 or records a version wrongly. An accepted JAR replaces a file of the same final name, as the download always did.

      A version the catalogue lists but does not offer for public download (licence required) is refused before anything is requested, with its own line; the framework implements no licensing of its own.

      Parameters:
      idString - the module's identify-string
      versionOrNull - the version, or null for the latest version the catalogue offers this framework
      Returns:
      the outcome; a refusal the operator is told about has already been logged
    • installVersion

      private static PluginInstallUtils.InstallResult installVersion(CatalogueClient.Version metadata, String idString, String requested, String version, int level)
      The second half of installReporting(java.lang.String, java.lang.String): the version is known. A version the catalogue does not offer for public download is refused here, before any request, and every other version goes through the verified download.
    • downloadAndAdmit

      private static PluginInstallUtils.InstallResult downloadAndAdmit(String url, CatalogueClient.Version metadata, String fileName, String requested, String version, int level)
      Verifies the download, then checks it as an archive and by its declared api-version, and either renames it to fileName or deletes it. No partial file survives this method, whatever happens.
    • failureLine

      static String failureLine(String module, String version, VerifiedDownload.Kind kind)
      The one line an operator sees when a download did not pass its checks: the module, the version and the reason, and that nothing was installed. A withdrawn version, a transport failure and an integrity failure cannot be mistaken for one another; every integrity failure (hash, size, hash header, no usable hash or size) is the catalogue text PluginInstallUtils.InstallResult.Keys.REASON_INTEGRITY_FAILED in the server's language (plan 22-22), and which check failed is on the FINE line beside it. Only short hash prefixes may join it in the local log, never a whole URL.
      Parameters:
      module - the module as the operator asked for it
      version - the version
      kind - why the download was refused
      Returns:
      the line
    • isIntegrityFailure

      private static boolean isIntegrityFailure(VerifiedDownload.Kind kind)
      Parameters:
      kind - why a download was refused
      Returns:
      whether the bytes, the header or the catalogue entry failed the integrity check, as opposed to a version that is not offered or a download that did not complete
    • licenceRequired

      private static boolean licenceRequired(CatalogueClient.Version metadata)
      Returns:
      whether the catalogue offers the version only with a licence
    • foreignDownloadLine

      private static String foreignDownloadLine(String module, String version)
      The line for a download.url the catalogue put outside its own host: refused before anything is requested, because a catalogue answer must never move an executable download.
    • notAvailableLine

      private static String notAvailableLine(String module, String version)
      The line for a version the catalogue lists but does not offer for another reason than a licence (that one is PluginInstallUtils.InstallResult.Keys.REASON_LICENCE_REQUIRED); it is not an integrity failure.
    • 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
    • downloadVerified

      static void downloadVerified(String link, CatalogueClient.Version metadata, String fileName, File folder) throws IOException
      The staging download: the same verification the install applies, because a staged update becomes the module that loads at the next start. The link is the version's download.url, used as written; the metadata it is checked against is the version object the caller has just read through the contract's version route, so staging never trusts a hash it did not just fetch.
      Parameters:
      link - the absolute version download URL
      metadata - the version, as the catalogue's version route just answered it
      fileName - the file name to write
      folder - the folder to write it into
      Throws:
      IOException - when the catalogue offers no public download or the file does not verify
    • reasonOf

      static String reasonOf(VerifiedDownload.Kind kind)
      Returns:
      the reason a staged download was refused, as the update reason names it
    • 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