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:
Michael Jumper
2015-05-10 21:10:56 -07:00
parent 6b4e798c6c
commit 8ae9a2e28c
3 changed files with 456 additions and 10 deletions

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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)));
}