mirror of
				https://github.com/gyurix1968/guacamole-client.git
				synced 2025-10-31 00:53:21 +00:00 
			
		
		
		
	GUAC-587: Define class representing extensions and the extension manifest file. Dynamically add JavaScript and CSS resources to app.js and app.css.
This commit is contained in:
		| @@ -0,0 +1,241 @@ | ||||
| /* | ||||
|  * Copyright (C) 2015 Glyptodon LLC | ||||
|  * | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  * | ||||
|  * The above copyright notice and this permission notice shall be included in | ||||
|  * all copies or substantial portions of the Software. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||
|  * THE SOFTWARE. | ||||
|  */ | ||||
|  | ||||
| package org.glyptodon.guacamole.net.basic.extension; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.net.MalformedURLException; | ||||
| import java.net.URL; | ||||
| import java.net.URLClassLoader; | ||||
| import java.security.AccessController; | ||||
| import java.security.PrivilegedActionException; | ||||
| import java.security.PrivilegedExceptionAction; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
| import java.util.zip.ZipEntry; | ||||
| import java.util.zip.ZipException; | ||||
| import java.util.zip.ZipFile; | ||||
| import org.codehaus.jackson.JsonParseException; | ||||
| import org.codehaus.jackson.map.ObjectMapper; | ||||
| import org.glyptodon.guacamole.GuacamoleException; | ||||
| import org.glyptodon.guacamole.GuacamoleServerException; | ||||
| import org.glyptodon.guacamole.net.basic.resource.ClassPathResource; | ||||
| import org.glyptodon.guacamole.net.basic.resource.Resource; | ||||
|  | ||||
| /** | ||||
|  * A Guacamole extension, which may provide custom authentication, static | ||||
|  * files, theming/branding, etc. | ||||
|  * | ||||
|  * @author Michael Jumper | ||||
|  */ | ||||
| public class Extension { | ||||
|  | ||||
|     /** | ||||
|      * The Jackson parser for parsing the language JSON files. | ||||
|      */ | ||||
|     private static final ObjectMapper mapper = new ObjectMapper(); | ||||
|  | ||||
|     /** | ||||
|      * The name of the manifest file that describes the contents of a | ||||
|      * Guacamole extension. | ||||
|      */ | ||||
|     private static final String MANIFEST_NAME = "guac-manifest.json"; | ||||
|  | ||||
|     /** | ||||
|      * The parsed manifest file of this extension, describing the location of | ||||
|      * resources within the extension. | ||||
|      */ | ||||
|     private final ExtensionManifest manifest; | ||||
|  | ||||
|     /** | ||||
|      * The classloader to use when reading resources from this extension, | ||||
|      * including classes and static files. | ||||
|      */ | ||||
|     private final ClassLoader classLoader; | ||||
|  | ||||
|     /** | ||||
|      * Loads the given file as an extension, which must be a .jar containing | ||||
|      * a guac-manifest.json file describing its contents. | ||||
|      * | ||||
|      * @param parent | ||||
|      *     The classloader to use as the parent for the isolated classloader of | ||||
|      *     this extension. | ||||
|      * | ||||
|      * @param file | ||||
|      *     The file to load as an extension. | ||||
|      * | ||||
|      * @throws GuacamoleException | ||||
|      *     If the provided file is not a .jar file, does not contain the | ||||
|      *     guac-manifest.json, or if guac-manifest.json is invalid and cannot | ||||
|      *     be parsed. | ||||
|      */ | ||||
|     public Extension(final ClassLoader parent, final File file) throws GuacamoleException { | ||||
|  | ||||
|         try { | ||||
|  | ||||
|             // Open extension | ||||
|             ZipFile extension = new ZipFile(file); | ||||
|  | ||||
|             try { | ||||
|  | ||||
|                 // Retrieve extension manifest | ||||
|                 ZipEntry manifestEntry = extension.getEntry(MANIFEST_NAME); | ||||
|                 if (manifestEntry == null) | ||||
|                     throw new GuacamoleServerException("Extension " + file.getName() + " is missing " + MANIFEST_NAME); | ||||
|  | ||||
|                 // Parse manifest | ||||
|                 manifest = mapper.readValue(extension.getInputStream(manifestEntry), ExtensionManifest.class); | ||||
|  | ||||
|             } | ||||
|  | ||||
|             // Always close zip file, if possible | ||||
|             finally { | ||||
|                 extension.close(); | ||||
|             } | ||||
|  | ||||
|             try { | ||||
|  | ||||
|                 // Create isolated classloader for this extension | ||||
|                 classLoader = AccessController.doPrivileged(new PrivilegedExceptionAction<ClassLoader>() { | ||||
|  | ||||
|                     @Override | ||||
|                     public ClassLoader run() throws GuacamoleException { | ||||
|  | ||||
|                         try { | ||||
|  | ||||
|                             // Classloader must contain only the extension itself | ||||
|                             return new URLClassLoader(new URL[]{file.toURI().toURL()}, parent); | ||||
|  | ||||
|                         } | ||||
|                         catch (MalformedURLException e) { | ||||
|                             throw new GuacamoleException(e); | ||||
|                         } | ||||
|  | ||||
|                     } | ||||
|  | ||||
|                 }); | ||||
|  | ||||
|             } | ||||
|  | ||||
|             // Rethrow any GuacamoleException | ||||
|             catch (PrivilegedActionException e) { | ||||
|                 throw (GuacamoleException) e.getException(); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         // Abort load if not a valid zip file | ||||
|         catch (ZipException e) { | ||||
|             throw new GuacamoleServerException("Extension is not a valid zip file: " + file.getName(), e); | ||||
|         } | ||||
|  | ||||
|         // Abort if manifest cannot be parsed (invalid JSON) | ||||
|         catch (JsonParseException e) { | ||||
|             throw new GuacamoleServerException(MANIFEST_NAME + " is not valid JSON: " + file.getName(), e); | ||||
|         } | ||||
|  | ||||
|         // Abort if zip file cannot be read at all due to I/O errors | ||||
|         catch (IOException e) { | ||||
|             throw new GuacamoleServerException("Unable to read extension: " + file.getName(), e); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the name of this extension, as declared in the extension's | ||||
|      * manifest. | ||||
|      * | ||||
|      * @return | ||||
|      *     The name of this extension. | ||||
|      */ | ||||
|     public String getName() { | ||||
|         return manifest.getName(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the namespace of this extension, as declared in the extension's | ||||
|      * manifest. | ||||
|      * | ||||
|      * @return | ||||
|      *     The namespace of this extension. | ||||
|      */ | ||||
|     public String getNamespace() { | ||||
|         return manifest.getNamespace(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns a new collection of resources corresponding to the collection of | ||||
|      * paths provided. Each resource will be associated with the given | ||||
|      * mimetype. | ||||
|      * | ||||
|      * @param mimetype | ||||
|      *     The mimetype to associate with each resource. | ||||
|      * | ||||
|      * @param paths | ||||
|      *     The paths corresponding to the resources desired. | ||||
|      * | ||||
|      * @return | ||||
|      *     A new, unmodifiable collection of resources corresponding to the | ||||
|      *     collection of paths provided. | ||||
|      */ | ||||
|     private Collection<Resource> getClassPathResources(String mimetype, Collection<String> paths) { | ||||
|  | ||||
|         // If no paths are provided, just return an empty list | ||||
|         if (paths == null) | ||||
|             return Collections.<Resource>emptyList(); | ||||
|  | ||||
|         // Add classpath resource for each path provided | ||||
|         Collection<Resource> resources = new ArrayList<Resource>(paths.size()); | ||||
|         for (String path : paths) | ||||
|             resources.add(new ClassPathResource(classLoader, mimetype, path)); | ||||
|  | ||||
|         // Callers should not rely on modifying the result | ||||
|         return Collections.unmodifiableCollection(resources); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns all declared JavaScript resources associated with this | ||||
|      * extension. JavaScript resources are declared within the extension | ||||
|      * manifest. | ||||
|      * | ||||
|      * @return | ||||
|      *     All declared JavaScript resources associated with this extension. | ||||
|      */ | ||||
|     public Collection<Resource> getJavaScriptResources() { | ||||
|         return getClassPathResources("text/javascript", manifest.getJavaScriptPaths()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns all declared CSS resources associated with this extension. CSS | ||||
|      * resources are declared within the extension manifest. | ||||
|      * | ||||
|      * @return | ||||
|      *     All declared CSS resources associated with this extension. | ||||
|      */ | ||||
|     public Collection<Resource> getCSSResources() { | ||||
|         return getClassPathResources("text/css", manifest.getCSSPaths()); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,172 @@ | ||||
| /* | ||||
|  * Copyright (C) 2015 Glyptodon LLC | ||||
|  * | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  * | ||||
|  * The above copyright notice and this permission notice shall be included in | ||||
|  * all copies or substantial portions of the Software. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||
|  * THE SOFTWARE. | ||||
|  */ | ||||
|  | ||||
| package org.glyptodon.guacamole.net.basic.extension; | ||||
|  | ||||
| import java.util.Collection; | ||||
| import org.codehaus.jackson.annotate.JsonProperty; | ||||
|  | ||||
| /** | ||||
|  * Java representation of the JSON manifest contained within every Guacamole | ||||
|  * extension, identifying an extension and describing its contents. | ||||
|  * | ||||
|  * @author Michael Jumper | ||||
|  */ | ||||
| public class ExtensionManifest { | ||||
|  | ||||
|     /** | ||||
|      * The name of the extension associated with this manifest. The extension | ||||
|      * name is human-readable, and used for display purposes only. | ||||
|      */ | ||||
|     private String name; | ||||
|  | ||||
|     /** | ||||
|      * The namespace of the extension associated with this manifest. The | ||||
|      * extension namespace is required for internal use, and is used wherever | ||||
|      * extension-specific files or resources need to be isolated from those of | ||||
|      * other extensions. | ||||
|      */ | ||||
|     private String namespace; | ||||
|  | ||||
|     /** | ||||
|      * The paths of all JavaScript resources within the .jar of the extension | ||||
|      * associated with this manifest. | ||||
|      */ | ||||
|     private Collection<String> javaScriptPaths; | ||||
|  | ||||
|     /** | ||||
|      * The paths of all CSS resources within the .jar of the extension | ||||
|      * associated with this manifest. | ||||
|      */ | ||||
|     private Collection<String> cssPaths; | ||||
|  | ||||
|     /** | ||||
|      * Returns the name of the extension associated with this manifest. The | ||||
|      * name is human-readable, for display purposes only, and is defined within | ||||
|      * the manifest by the "name" property. | ||||
|      * | ||||
|      * @return | ||||
|      *     The name of the extension associated with this manifest. | ||||
|      */ | ||||
|     public String getName() { | ||||
|         return name; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets the name of the extension associated with this manifest. The name | ||||
|      * is human-readable, for display purposes only, and is defined within the | ||||
|      * manifest by the "name" property. | ||||
|      * | ||||
|      * @param name | ||||
|      *     The name of the extension associated with this manifest. | ||||
|      */ | ||||
|     public void setName(String name) { | ||||
|         this.name = name; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the namespace of the extension associated with this manifest. | ||||
|      * The namespace is required for internal use, and is used wherever | ||||
|      * extension-specific files or resources need to be isolated from those of | ||||
|      * other extensions. It is defined within the manifest by the "namespace" | ||||
|      * property. | ||||
|      * | ||||
|      * @return | ||||
|      *     The namespace of the extension associated with this manifest. | ||||
|      */ | ||||
|     public String getNamespace() { | ||||
|         return namespace; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets the namespace of the extension associated with this manifest. The | ||||
|      * namespace is required for internal use, and is used wherever extension- | ||||
|      * specific files or resources need to be isolated from those of other | ||||
|      * extensions. It is defined within the manifest by the "namespace" | ||||
|      * property. | ||||
|      * | ||||
|      * @param namespace | ||||
|      *     The namespace of the extension associated with this manifest. | ||||
|      */ | ||||
|     public void setNamespace(String namespace) { | ||||
|         this.namespace = namespace; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the paths to all JavaScript resources within the extension. | ||||
|      * These paths are defined within the manifest by the "js" property as an | ||||
|      * array of strings, where each string is a path relative to the root of | ||||
|      * the extension .jar. | ||||
|      * | ||||
|      * @return | ||||
|      *     A collection of paths to all JavaScript resources within the | ||||
|      *     extension. | ||||
|      */ | ||||
|     @JsonProperty("js") | ||||
|     public Collection<String> getJavaScriptPaths() { | ||||
|         return javaScriptPaths; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets the paths to all JavaScript resources within the extension. These | ||||
|      * paths are defined within the manifest by the "js" property as an array | ||||
|      * of strings, where each string is a path relative to the root of the | ||||
|      * extension .jar. | ||||
|      * | ||||
|      * @param javaScriptPaths | ||||
|      *     A collection of paths to all JavaScript resources within the | ||||
|      *     extension. | ||||
|      */ | ||||
|     @JsonProperty("js") | ||||
|     public void setJavaScriptPaths(Collection<String> javaScriptPaths) { | ||||
|         this.javaScriptPaths = javaScriptPaths; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the paths to all CSS resources within the extension. These paths | ||||
|      * are defined within the manifest by the "js" property as an array of | ||||
|      * strings, where each string is a path relative to the root of the | ||||
|      * extension .jar. | ||||
|      * | ||||
|      * @return | ||||
|      *     A collection of paths to all CSS resources within the extension. | ||||
|      */ | ||||
|     @JsonProperty("css") | ||||
|     public Collection<String> getCSSPaths() { | ||||
|         return cssPaths; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets the paths to all CSS resources within the extension. These paths | ||||
|      * are defined within the manifest by the "js" property as an array of | ||||
|      * strings, where each string is a path relative to the root of the | ||||
|      * extension .jar. | ||||
|      * | ||||
|      * @param cssPaths | ||||
|      *     A collection of paths to all CSS resources within the extension. | ||||
|      */ | ||||
|     @JsonProperty("css") | ||||
|     public void setCSSPaths(Collection<String> cssPaths) { | ||||
|         this.cssPaths = cssPaths; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -25,8 +25,13 @@ package org.glyptodon.guacamole.net.basic.extension; | ||||
| import com.google.inject.servlet.ServletModule; | ||||
| import java.io.File; | ||||
| import java.io.FileFilter; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collection; | ||||
| import org.glyptodon.guacamole.GuacamoleException; | ||||
| import org.glyptodon.guacamole.environment.Environment; | ||||
| import org.glyptodon.guacamole.net.basic.resource.Resource; | ||||
| import org.glyptodon.guacamole.net.basic.resource.ResourceServlet; | ||||
| import org.glyptodon.guacamole.net.basic.resource.SequenceResource; | ||||
| import org.glyptodon.guacamole.net.basic.resource.WebApplicationResource; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| @@ -55,7 +60,7 @@ public class ExtensionModule extends ServletModule { | ||||
|      * recognized as extensions. | ||||
|      */ | ||||
|     private static final String EXTENSION_SUFFIX = ".jar"; | ||||
|      | ||||
|  | ||||
|     /** | ||||
|      * The Guacamole server environment. | ||||
|      */ | ||||
| @@ -81,7 +86,7 @@ public class ExtensionModule extends ServletModule { | ||||
|             return; | ||||
|  | ||||
|         // Retrieve list of all extension files within extensions directory | ||||
|         File[] extensions = extensionsDir.listFiles(new FileFilter() { | ||||
|         File[] extensionFiles = extensionsDir.listFiles(new FileFilter() { | ||||
|  | ||||
|             @Override | ||||
|             public boolean accept(File file) { | ||||
| @@ -90,15 +95,43 @@ public class ExtensionModule extends ServletModule { | ||||
|  | ||||
|         }); | ||||
|  | ||||
|         // Load each extension | ||||
|         for (File extension : extensions) { | ||||
|             // TODO: Actually load extension | ||||
|             logger.info("Loading extension: \"{}\"", extension.getName()); | ||||
|         // Init JavaScript resources with base guacamole.min.js | ||||
|         Collection<Resource> javaScriptResources = new ArrayList<Resource>(); | ||||
|         javaScriptResources.add(new WebApplicationResource(getServletContext(), "/guacamole.min.js")); | ||||
|  | ||||
|         // Init CSS resources with base guacamole.min.css | ||||
|         Collection<Resource> cssResources = new ArrayList<Resource>(); | ||||
|         cssResources.add(new WebApplicationResource(getServletContext(), "/guacamole.min.css")); | ||||
|  | ||||
|         // Load each extension within the extension directory | ||||
|         for (File extensionFile : extensionFiles) { | ||||
|  | ||||
|             logger.debug("Loading extension: \"{}\"", extensionFile.getName()); | ||||
|  | ||||
|             try { | ||||
|  | ||||
|                 // FIXME: Use class loader which reads from the lib directory | ||||
|                 // Load extension from file | ||||
|                 Extension extension = new Extension(ExtensionModule.class.getClassLoader(), extensionFile); | ||||
|  | ||||
|                 // Add any JavaScript / CSS resources | ||||
|                 javaScriptResources.addAll(extension.getJavaScriptResources()); | ||||
|                 cssResources.addAll(extension.getCSSResources()); | ||||
|  | ||||
|                 // Log successful loading of extension by name | ||||
|                 logger.info("Extension \"{}\" loaded.", extension.getName()); | ||||
|  | ||||
|             } | ||||
|             catch (GuacamoleException e) { | ||||
|                 logger.error("Extension \"{}\" could not be loaded: {}", extensionFile.getName(), e.getMessage()); | ||||
|                 logger.debug("Unable to load extension.", e); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|          | ||||
|         // TODO: Pull these from extensions, dynamically concatenated | ||||
|         serve("/app.js").with(new ResourceServlet(new WebApplicationResource(getServletContext(), "/guacamole.min.js"))); | ||||
|         serve("/app.css").with(new ResourceServlet(new WebApplicationResource(getServletContext(), "/guacamole.min.css"))); | ||||
|  | ||||
|         // Dynamically generate app.js and app.css from extensions | ||||
|         serve("/app.js").with(new ResourceServlet(new SequenceResource(javaScriptResources))); | ||||
|         serve("/app.css").with(new ResourceServlet(new SequenceResource(cssResources))); | ||||
|  | ||||
|     } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user