Class ExtensionManager<T extends AppExtension>
- Type Parameters:
T- Any class that implements AppExtension - this is the extension class we'll scan for. Your application could theoretically support more than one extension class type, but you would have to have a separate ExtensionManager derived class for each extension type.
Your extension should bundle all required code and resources into a jar file, and include an extInfo.json file in the jar resources. We scan for that file in this class and parse it out to see if a) it exists, and b) if the version requirements in the candidate extension jar are met. See extractExtInfo for more details.
Refer to the swing-extras documentation for more information.
- Since:
- 2023-11-11
- Author:
- scorbo2
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionprotected classThis is used internally to combine a source jar file, the extension that it contained, and an isEnabled status flag into one handy location.static classTracks any jar file that failed to load when ExtensionManager started up. -
Field Summary
Fields -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionvoidSends an onActivate() message to each enabled extension, to let them know that we're starting up - use deactivateAll() to signal shutdown.booleanaddExtension(T extension, boolean isEnabled) Programmatically adds an extension to our list - this was originally intended for testing purposes, but might be useful as a way for applications to supply built-in extensions without having to package them in separate jar files with the distribution.protected voidaddStartupError(File jarFile, String msg) Invoked internally to log a startup error which can be displayed on the ExtensionManagerDialog later.protected voidaddStartupError(File jarFile, String msg, Level logLevel, Throwable e) Invoked internally to log a startup error which can be displayed on the ExtensionManagerDialog later.voidSends an onDeactivate() message to each enabled extension, to let them know that we're shutting down - use activateAll() to signal startup.extractExtInfo(File jarFile) Invoked internally to look for an extInfo.json file inside the given jar file and attempt to parse an AppExtensionInfo object out of it.findCandidateExtensionJars(File directory, String appName, String requiredVersion) Scans the given directory looking for Jar files that contain an extInfo.json file, and if one is found, will check its parameters against the given appName and requiredVersion to make sure the extension would work for that application.findExtensionByName(String name) Searches for any loaded extension with a matching name (case-sensitive) and returns the first match.Similar to findExtensionByName, but will specifically return the Jar file from which the given extension was loaded, assuming it was loaded from a jar file.Invoke this to interrogate each enabled extension for their config properties, if any, and return them in a list.Returns a list of all loaded extensions - beware that this method will return extensions even if they are marked as disabled!protected List<ExtensionManager<T>.ExtensionWrapper> Invoked internally to return a list of all loaded extension wrappers, sorted by the extension name.Returns the application name as it was supplied to the loadExtensions method, or null if loadExtensions has not yet been invoked.Returns the application version as it was supplied to the loadExtensions method, or null if loadExtensions has not yet been invoked.Returns a list of all loaded extensions that are marked as enabled.Applications will typically have a single directory where they store all extension jars, but this is not a hard requirement.getLoadedExtension(String className) Returns the extension instance for the given class name, if one exists.intReports how many extensions have been loaded.getSourceJar(String className) Returns the source jar from which the given extension was loaded, or null if this extension was added manually with addExtension().After loadExtensions is invoked, this method will return a list of any StartupErrors that were detected (jar files that failed to load).booleanisExtensionEnabled(String className) Reports whether the loaded extension with the given class name is enabled.booleanisExtensionLoaded(String className) Reports whether an extension with the given class name is currently loaded.booleanjarFileMeetsRequirements(File jarFile, AppExtensionInfo extInfo, String appName, String requiredVersion) Checks if the given jar file and extension info meet the given requirements (that is, that the application name and version requirements are met).loadExtensionFromJar(File jarFile, Class<T> extensionClass) Scans the given jar file looking for any classes that match extensionClass.intScans the given directory looking for candidate jar files that contain an extension matching the given parameters.voidsetExtensionEnabled(String className, boolean isEnabled) Enables or disables the named extension - this involves sending an onActivate or onDeactivate message to the extension as needed.voidsetExtensionEnabled(String className, boolean isEnabled, boolean notify) Enables or disabled the named extension - if notify is true, will send the extension an onActivate or onDeactivate message as needed.voidsetExtensionsDirectory(File extensionsDirectory) Sets the directory where the application wishes to store extension jars.sortExtensionJarSet(File directory, Set<File> jarSet) Given a Set of jar files in a given directory, this method looks for an optional load order control file and will attempt to obey any sorting directives it contains.intRemoves all extensions that were previously loaded in this ExtensionManager, and returns it to an empty state.booleanunloadExtension(String className) Unregisters the given extension, if it was registered.
-
Field Details
-
logger
-
-
Constructor Details
-
ExtensionManager
public ExtensionManager()
-
-
Method Details
-
getLoadedExtensionCount
public int getLoadedExtensionCount()Reports how many extensions have been loaded.- Returns:
- A count of loaded extensions (enabled or not).
-
getStartupErrors
After loadExtensions is invoked, this method will return a list of any StartupErrors that were detected (jar files that failed to load). -
getExtensionsDirectory
Applications will typically have a single directory where they store all extension jars, but this is not a hard requirement. This method returns the last directory that was given to loadExtensions(), or whatever directory was last given to setExtensionsDirectory(), whichever occurred more recently. If your application supports scanning multiple extension directories, you should probably manage that in your application code rather than relying on this method. -
setExtensionsDirectory
Sets the directory where the application wishes to store extension jars. This is implicitly overwritten on every call to loadExtensions(). -
getApplicationName
Returns the application name as it was supplied to the loadExtensions method, or null if loadExtensions has not yet been invoked. -
getApplicationVersion
Returns the application version as it was supplied to the loadExtensions method, or null if loadExtensions has not yet been invoked. -
isExtensionLoaded
Reports whether an extension with the given class name is currently loaded. Note you can also do getLoadedExtension(className) and check for null. Even if this method returns true, the extension might not be enabled! Use isExtensionEnabled() to determine if the extension is actually enabled.- Parameters:
className- The fully qualified class name of the extension to look for.- Returns:
- True if the named extension has been loaded by this manager.
-
isExtensionEnabled
Reports whether the loaded extension with the given class name is enabled.- Parameters:
className- the fully qualified class name of the extension in question.- Returns:
- True if the extension is enabled, false if disabled or not found.
-
setExtensionEnabled
Enables or disables the named extension - this involves sending an onActivate or onDeactivate message to the extension as needed. If the given isEnabled value matches the existing value, no action is taken. If the given extension cannot be found, no action is taken.- Parameters:
className- The fully qualified class name of the extension in question.isEnabled- Whether to enable or disable the extension.
-
setExtensionEnabled
Enables or disabled the named extension - if notify is true, will send the extension an onActivate or onDeactivate message as needed. If the given isEnabled value matches the existing value, no action is taken. If the given extension cannot be found, no action is taken.- Parameters:
className- The fully qualified class name of the extension in question.isEnabled- Whether to enable or disable the extension.notify- Whether to send a message to the extension notifying them of the change.
-
getSourceJar
Returns the source jar from which the given extension was loaded, or null if this extension was added manually with addExtension().- Parameters:
className- the fully qualified class name of the extension in question.- Returns:
- The File representing the extensions source jar, or null if not found or built-in.
-
getLoadedExtension
Returns the extension instance for the given class name, if one exists.- Parameters:
className- the fully qualified class name of the extension in question.- Returns:
- The extension instance, or null if not found.
-
findExtensionByName
Searches for any loaded extension with a matching name (case-sensitive) and returns the first match. Will return null if no extensions are loaded or no match is found.- Parameters:
name- The extension name (as reported byextension.getInfo().getName()).- Returns:
- The extension instance, or null if not found.
-
findExtensionJarByExtensionName
Similar to findExtensionByName, but will specifically return the Jar file from which the given extension was loaded, assuming it was loaded from a jar file. If the named extension is not found, null is returned.NOTE: Built-in extensions will return null! This does not mean that the extension in question does not exist or was not found. We return null because built-in extensions were not loaded from a jar file, so there's nothing to return here. To test for the existence of an extension in this ExtensionManager, it is far safer to use isExtensionLoaded() or isExtensionEnabled() instead.
-
getAllLoadedExtensions
Returns a list of all loaded extensions - beware that this method will return extensions even if they are marked as disabled! If you only want to get the extensions that are currently enabled, use getEnabledLoadedExtensions() instead. The list is sorted by extension name (not by class name).- Returns:
- A List of zero or more extensions sorted by extension name.
-
getEnabledLoadedExtensions
Returns a list of all loaded extensions that are marked as enabled. This list is computed each time this method is called as extensions can be enabled and disabled at pretty much any point. The list is sorted by extension name (not by class name).- Returns:
- A List of zero or more enabled and loaded extensions sorted by extension name.
-
getAllEnabledExtensionProperties
Invoke this to interrogate each enabled extension for their config properties, if any, and return them in a list. The properties are grouped together into a single list that is ordered first by extension name (not class name), and then by the order in which the properties were defined within each extension.Note: If two or more extensions define a property with the same fully-qualified name, only the first one found will be returned. This is to avoid conflicts that would arise from having multiple properties with the same name.
- Returns:
- The combined list of properties of all enabled extensions.
-
addExtension
Programmatically adds an extension to our list - this was originally intended for testing purposes, but might be useful as a way for applications to supply built-in extensions without having to package them in separate jar files with the distribution. Remember that extension load order matters! If more than one extension provides the same functionality, it's up to the application to decide which extension's version of it should be used. By convention, the first loaded extension that supplies a given piece of functionality is the one that will be used. You can use the ext-load-order.txt file in your extension jar directory to explicitly decide which extensions are loaded in which order, but built-in extensions added through his mechanism bypass the ext-load-order check. So, consider carefully if your built-in extensions should be loaded BEFORE externally-loaded extensions (thereby giving them higher priority), or AFTER externally-loaded extensions (thereby giving them lower priority). There is no "default" behavior here... it's up to the application to decide whether to invoke addExtension() before or after loadExtensions().Note: The supplied extension will not receive an onActivate() notification from this method. Use activateAll() to start up extensions.
Note: the same extension validation rules apply here as for externally-loaded extensions. Your extension must supply a well-formed AppExtensionInfo instance containing extension name, version, target app name, and target app version. If any of these are missing or invalid, the extension is rejected and NOT loaded.
- Parameters:
extension- The extension instance to be added.isEnabled- Whether to enable this extension immediately or not.- Returns:
- Whether the extension was successfully added.
-
activateAll
public void activateAll()Sends an onActivate() message to each enabled extension, to let them know that we're starting up - use deactivateAll() to signal shutdown. -
deactivateAll
public void deactivateAll()Sends an onDeactivate() message to each enabled extension, to let them know that we're shutting down - use activateAll() to signal startup. -
unloadAllExtensions
public int unloadAllExtensions()Removes all extensions that were previously loaded in this ExtensionManager, and returns it to an empty state. Client applications should use this with caution - if you have queried for configuration properties from all extensions before invoking this, then you should once again invoke getAllEnabledExtensionProperties() because the list of properties will likely decrease as extensions are unloaded. Your UI should remove the properties that no longer exist.- Returns:
- The count of extensions that were actually removed as a result of this call.
-
unloadExtension
Unregisters the given extension, if it was registered. The extension will receive a deactivate() message before it is unloaded, if it was enabled. Client applications should use this with caution - if you have queried for configuration properties from all extensions before invoking this, then you should once again invoke getAllEnabledExtensionProperties() because the list of properties will likely decrease as extensions are unloaded. Your UI should remove the properties that no longer exist.- Parameters:
className- The fully qualified class name of the extension to be unregistered.- Returns:
- true if an extension was actually removed as a result of this call.
-
loadExtensions
public int loadExtensions(File directory, Class<T> extClass, String appName, String requiredVersion) Scans the given directory looking for candidate jar files that contain an extension matching the given parameters. For each jar that is found, an attempt will be made to load the extension class out of that jar file. All successfully loaded extension classes will then be loaded into this ExtensionManager.Note that this is a shorthand way of doing this more manually (or jar by jar) via the findCandidateExtensionJars, extractExtInfo, and jarFileMeetsRequirements methods. Generally, this is the better entry point, but if you have a specific jar file that you want to scan and load from, those other methods can be used instead.
A note about versioning: In swing-extras 2.5 and previous releases, the extension's target application version must match the application's version exactly in order for the extension to be considered a match. This was found to be too restrictive, because each time an application releases a new version, all existing extensions must be re-released even if they are perfectly compatible with the new version. Starting in the 2.6 release of swing-extras, an extension will be considered compatible if its target application version matches the major version of the application. This requires applications to adopt a versioning convention where they release a minor version only if there are no extension-breaking changes, otherwise they must release a new major version. For example, if an application is at version 3.2, then extensions targeting version 3.0, 3.1, 3.5, etc. will be considered compatible, but extensions targeting version 2.x or 4.x will not be considered compatible.
- Parameters:
directory- The directory to scan. Must not be null.extClass- The AppExtension implementation class to look for.appName- The application name to match against, or null to skip this check.requiredVersion- The required app version that the extension must target. (Only the major version is considered here). May be null to skip this check.- Returns:
- The count of extensions that were loaded by this operation.
-
findCandidateExtensionJars
public Map<File,AppExtensionInfo> findCandidateExtensionJars(File directory, String appName, String requiredVersion) Scans the given directory looking for Jar files that contain an extInfo.json file, and if one is found, will check its parameters against the given appName and requiredVersion to make sure the extension would work for that application. The return is a Map of File to AppExtensionInfo, which can then be loaded via one of the loadExtension methods. Note that this method does not actually try to load the extension, it simply scans to find which jar file would be good candidates for loading. This can therefore be used for autodiscovery of extension jars in a given directory safely, without actually loading them. Note that both appName and requiredVersion are optional (you can pass null to disable those checks), but the result may be jar files that contain extensions for the wrong app, or for the wrong version of the app, or both.- Parameters:
directory- The directory to scan (will be scanned recursively).appName- The application name to check for, or null to skip this check.requiredVersion- The required app version, or null to skip this check.- Returns:
- A Map of jar files to AppExtensionInfo objects.
-
jarFileMeetsRequirements
public boolean jarFileMeetsRequirements(File jarFile, AppExtensionInfo extInfo, String appName, String requiredVersion) Checks if the given jar file and extension info meet the given requirements (that is, that the application name and version requirements are met). This does not guarantee that an extension can be successfully loaded out of the given jar file, but it is a pretty good indicator.- Parameters:
jarFile- The jar file in question.extInfo- The extension info that was extracted from that jar via extractExtInfoappName- The name of the application to check for.requiredVersion- The app version that the extension must target. (Only the major version is considered)- Returns:
- true if the jar file looks good, false otherwise.
-
loadExtensionFromJar
Scans the given jar file looking for any classes that match extensionClass. The first matching class found will be loaded as an extension of type T and returned. Multiple extension implementations in the same jar file are therefore not supported! If you have several extensions to provide for an application, package each one into its own jar file. (This is better practice anyway, as it allows you to independently version and release each one).- Parameters:
jarFile- The jar file to scan.extensionClass- The implementing class to look for.- Returns:
- An implementation of T if one could be found and loaded, otherwise null.
-
extractExtInfo
Invoked internally to look for an extInfo.json file inside the given jar file and attempt to parse an AppExtensionInfo object out of it. Upon success, the newly created AppExtensionInfo is returned. If anything goes wrong, the error is logged and null is returned.Packaging an extInfo.json file into your extension jar
Your jar file should contain an extInfo.json file somewhere in its resources. We'll scan every entry in the jar file looking for it, so the exact location doesn't matter, but a good convention is: resources/fully/qualified/main/package/extInfo.jsonTip: You can easily generate an extInfo.json by populating an AppExtensionInfo object in code and invoking toJson() on it, instead of writing the JSON by hand.
- Parameters:
jarFile- The jar file in question. Must not be null.- Returns:
- An AppExtensionInfo, or null.
-
sortExtensionJarSet
Given a Set of jar files in a given directory, this method looks for an optional load order control file and will attempt to obey any sorting directives it contains. If the file is missing or incomplete, the input set will be sorted by filename. If the load order file mentions any jar files that don't exist in that directory, those directives are ignored.Formatting the load order file
Blank lines and lines starting with a hash character are ignored. All other lines in the file are assumed to be either the exact name (without path) of a single jar file, or a partial name that matches the start of a jar filename. The order in which those jars are listed in this file is the order that the extension jars will be loaded.Best practice: Use partial filename matches (without version numbers) rather than exact filenames. This makes the load order file resilient to extension version upgrades. For example, use "extension7" instead of "extension7-1.0.0.jar" so the load order continues to work when the extension is upgraded to version 2.0.0. The matching logic first attempts an exact match, then falls back to partial matching using String.startsWith(). If multiple jars match a partial pattern, the first match alphabetically is selected.
An example file might look like this:
# Extension load order for MyApplication: # Extension 7 is super important, so let's load it first: extension7 extension2 # Extension 1 is not so important, so let's load it last: extension1
SPECIAL NOTE: the load order for application built-in extensions can't be overridden in this load order control file! Refer to the Javadocs for addExtension() for more information on this.
- Parameters:
directory- The directory to scanjarSet- The Set of jar files to consider within that directory- Returns:
- A sorted List of jar files. This list.size() will always match the input Set's size.
-
getAllLoadedExtensionWrappers
Invoked internally to return a list of all loaded extension wrappers, sorted by the extension name.- Returns:
- A List of ExtensionWrappers, sorted by extension name (not by class name)
-
addStartupError
Invoked internally to log a startup error which can be displayed on the ExtensionManagerDialog later. -
addStartupError
Invoked internally to log a startup error which can be displayed on the ExtensionManagerDialog later.
-