mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +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