diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java b/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java index 3183fa21c..ac58676ec 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java @@ -35,6 +35,8 @@ import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; + +import org.apache.guacamole.net.event.listener.Listener; import org.codehaus.jackson.JsonParseException; import org.codehaus.jackson.map.ObjectMapper; import org.apache.guacamole.GuacamoleException; @@ -109,6 +111,11 @@ public class Extension { */ private final Collection> authenticationProviderClasses; + /** + * The collection of all Listener classes defined within the extension. + */ + private final Collection> listenerClasses; + /** * The resource for the small favicon for the extension. If provided, this * will replace the default Guacamole icon. @@ -265,6 +272,80 @@ public class Extension { } + /** + * Retrieve the Listener subclass having the given name. If + * the class having the given name does not exist or isn't actually a + * subclass of Listener, an exception will be thrown. + * + * @param name + * The name of the Listener class to retrieve. + * + * @return + * The subclass of Listener having the given name. + * + * @throws GuacamoleException + * If no such class exists, or if the class with the given name is not + * a subclass of Listener. + */ + @SuppressWarnings("unchecked") // We check this ourselves with isAssignableFrom() + private Class getListenerClass(String name) + throws GuacamoleException { + + try { + + // Get listener class + Class listenerClass = classLoader.loadClass(name); + + // Verify the located class is actually a subclass of Listener + if (!Listener.class.isAssignableFrom(listenerClass)) + throw new GuacamoleServerException("Listeners MUST implement a Listener subclass."); + + // Return located class + return (Class) listenerClass; + + } + catch (ClassNotFoundException e) { + throw new GuacamoleException("Listener class not found.", e); + } + catch (LinkageError e) { + throw new GuacamoleException("Listener class cannot be loaded (wrong version of API?).", e); + } + + } + + /** + * Returns a new collection of all Listener subclasses having the given names. + * If any class does not exist or isn't actually subclass of Listener, an + * exception will be thrown, an no further Listener classes will be loaded. + * + * @param names + * The names of the AuthenticationProvider classes to retrieve. + * + * @return + * A new collection of all AuthenticationProvider subclasses having the + * given names. + * + * @throws GuacamoleException + * If any given class does not exist, or if any given class is not a + * subclass of AuthenticationProvider. + */ + private Collection> getListenerClasses(Collection names) + throws GuacamoleException { + + // If no classnames are provided, just return an empty list + if (names == null) + return Collections.>emptyList(); + + // Define all auth provider classes + Collection> classes = new ArrayList>(names.size()); + for (String name : names) + classes.add(getListenerClass(name)); + + // Callers should not rely on modifying the result + return Collections.unmodifiableCollection(classes); + } + + /** * Loads the given file as an extension, which must be a .jar containing * a guac-manifest.json file describing its contents. @@ -363,6 +444,9 @@ public class Extension { // Define authentication providers authenticationProviderClasses = getAuthenticationProviderClasses(manifest.getAuthProviders()); + // Define listeners + listenerClasses = getListenerClasses(manifest.getListeners()); + // Get small icon resource if provided if (manifest.getSmallIcon() != null) smallIcon = new ClassPathResource(classLoader, "image/png", manifest.getSmallIcon()); @@ -488,6 +572,17 @@ public class Extension { return authenticationProviderClasses; } + /** + * Returns all declared listener classes associated wit this extension. Listeners are + * declared within the extension manifest. + * + * @return + * All declared listener classes with this extension. + */ + public Collection> getListenerClasses() { + return listenerClasses; + } + /** * Returns the resource for the small favicon for the extension. If * provided, this will replace the default Guacamole icon. diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionManifest.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionManifest.java index 9b9bd9bee..1636b0350 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionManifest.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionManifest.java @@ -87,6 +87,11 @@ public class ExtensionManifest { */ private Collection authProviders; + /** + * The names of all listener classes within this extension, if any. + */ + private Collection listeners; + /** * The path to the small favicon. If provided, this will replace the default * Guacamole icon. @@ -355,6 +360,32 @@ public class ExtensionManifest { this.authProviders = authProviders; } + /** + * Returns the classnames of all listener classes within the extension. + * These classnames are defined within the manifest by the "listeners" + * property as an array of strings, where each string is a listener + * class name. + * + * @return + * a collection of classnames for all listeners within the extension + */ + public Collection getListeners() { + return listeners; + } + + /** + * Sets the classnames of all listener classes within the extension. + * These classnames are defined within the manifest by the "listeners" + * property as an array of strings, where each string is a listener + * class name. + * + * @param listeners + * a collection of classnames for all listeners within the extension + */ + public void setListeners(Collection listeners) { + this.listeners = listeners; + } + /** * Returns the path to the small favicon, relative to the root of the * extension. diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java index 792066c87..1e1a85452 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java @@ -34,6 +34,7 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.net.auth.AuthenticationProvider; +import org.apache.guacamole.net.event.listener.Listener; import org.apache.guacamole.resource.Resource; import org.apache.guacamole.resource.ResourceServlet; import org.apache.guacamole.resource.SequenceResource; @@ -91,6 +92,12 @@ public class ExtensionModule extends ServletModule { private final List boundAuthenticationProviders = new ArrayList(); + /** + * All currently-bound authentication providers, if any. + */ + private final List boundListenerProviders = + new ArrayList(); + /** * Service for adding and retrieving language resources. */ @@ -187,6 +194,49 @@ public class ExtensionModule extends ServletModule { return Collections.unmodifiableList(boundAuthenticationProviders); } + /** + * Binds the given Listener class such that any service + * requiring access to the Listener can obtain it via + * injection, along with any other bound Listener. + * + * @param listenerClass + * The Listener class to bind. + */ + private void bindListenerProvider(Class listenerClass) { + + // Bind listener + logger.debug("[{}] Binding Listener \"{}\".", + boundListenerProviders.size(), listenerClass.getName()); + boundListenerProviders.add(new ListenerFacade(listenerClass)); + } + + /** + * Binds each of the the given Listener classes such that any + * service requiring access to the Listener can obtain it via + * injection. + * + * @param listeners + * The Listener classes to bind. + */ + private void bindListenerProviders(Collection> listeners) { + + // Bind each listener within extension + for (Class listener : listeners) + bindListenerProvider(listener); + } + + /** + * Returns a list of all currently-bound ListenerProvider instances. + * + * @return + * A List of all currently-bound ListenerProvider instances. The List is + * not modifiable. + */ + @Provides + public List getListenerProviders() { + return Collections.unmodifiableList(boundListenerProviders); + } + /** * Serves each of the given resources as a language resource. Language * resources are served from within the "/translations" directory as JSON @@ -327,6 +377,9 @@ public class ExtensionModule extends ServletModule { // Attempt to load all authentication providers bindAuthenticationProviders(extension.getAuthenticationProviderClasses()); + // Attempt to load all listeners + bindListenerProviders(extension.getListenerClasses()); + // Add any translation resources serveLanguageResources(extension.getTranslationResources());