blob: 6473aa23047bd1fd00ea9c2bcf40d39ca958f04a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.extension;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ClassLoader implementation which prioritizes the classes defined within a
* given extension .jar file. Unlike the standard URLClassLoader, classes
* within the parent ClassLoader are only used if they are not defined within
* the given .jar. If classes are defined in both the parent and the extension
* .jar, the versions defined within the extension .jar are used.
*/
public class ExtensionClassLoader extends URLClassLoader {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ExtensionClassLoader.class);
/**
* The prefix that should be given to the temporary directory containing
* all library .jar files that were bundled with the extension.
*/
private static final String EXTENSION_TEMP_DIR_PREFIX = "guac-extension-lib-";
/**
* The prefix that should be given to any files created for temporary
* storage of a library .jar file that was bundled with the extension.
*/
private static final String EXTENSION_TEMP_LIB_PREFIX = "bundled-";
/**
* The ClassLoader to use if class resolution through the extension .jar
* fails.
*/
private final ClassLoader parent;
/**
* Returns the URL that refers to the given file. If the given file refers
* to a directory, an exception is thrown.
*
* @param file
* The file to determine the URL of.
*
* @return
* A URL that refers to the given file.
*
* @throws GuacamoleException
* If the given file refers to a directory.
*/
private static URL getFileURL(File file) throws GuacamoleException {
// Validate extension-related file is indeed a file
if (!file.isFile())
throw new GuacamoleServerException("\"" + file + "\" is not a file.");
try {
return file.toURI().toURL();
}
catch (MalformedURLException e) {
throw new GuacamoleServerException(e);
}
}
/**
* Copies all bytes of data from a file within a .jar to a destination
* file.
*
* @param jar
* The JarFile containing the file to be copied.
*
* @param source
* The JarEntry representing the file to be copied within the given
* JarFile.
*
* @param dest
* The destination file that the data should be copied to.
*
* @throws IOException
* If an error occurs reading from the source .jar or writing to the
* destination file.
*/
private static void copyEntryToFile(JarFile jar, JarEntry source, File dest)
throws IOException {
int length;
byte[] buffer = new byte[8192];
try (InputStream input = jar.getInputStream(source)) {
try (OutputStream output = new FileOutputStream(dest)) {
while ((length = input.read(buffer)) > 0) {
output.write(buffer, 0, length);
}
}
}
}
/**
* Returns the URLs for the .jar files relevant to the given extension .jar
* file. Unless the extension bundles additional Java libraries, only the
* URL of the extension .jar will be returned. If additional Java libraries
* are bundled within the extension, URLs for those libraries will be
* included, as well. Temporary directories and/or files will be created as
* necessary to house bundled libraries. Only .jar files located directly
* within the root of the main extension .jar are considered.
*
* @param extension
* The extension .jar file to generate URLs for.
*
* @param temporaryFiles
* A modifiable List that should be populated with all temporary files
* created for the given extension. These files should be deleted on
* application shutdown in reverse order.
*
* @return
* An array of all URLs relevant to the given extension .jar.
*
* @throws GuacamoleException
* If the given file is not actually a file, the contents of the file
* cannot be read, or any necessary temporary files/directories cannot
* be created.
*/
private static URL[] getExtensionURLs(File extension,
List<File> temporaryFiles) throws GuacamoleException {
try (JarFile extensionJar = new JarFile(extension)) {
// Include extension itself within classpath
List<URL> urls = new ArrayList<>();
urls.add(getFileURL(extension));
Path extensionTempLibDir = null;
// Iterate through all entries (files) within the extension .jar,
// adding any nested .jar files within the archive root to the
// classpath
Enumeration<JarEntry> entries = extensionJar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
// Consider only .jar files located in root of archive
if (entry.isDirectory() ||! name.endsWith(".jar") || name.indexOf('/') != -1)
continue;
// Create temporary directory for housing this extension's
// bundled .jar files, if not already created
try {
if (extensionTempLibDir == null) {
extensionTempLibDir = Files.createTempDirectory(EXTENSION_TEMP_DIR_PREFIX);
temporaryFiles.add(extensionTempLibDir.toFile());
extensionTempLibDir.toFile().deleteOnExit();
}
}
catch (IOException e) {
throw new GuacamoleServerException("Temporary directory "
+ "for libraries bundled with extension \""
+ extension + "\" could not be created.", e);
}
// Create temporary file to hold the contents of the current
// bundled .jar
File tempLibrary;
try {
tempLibrary = Files.createTempFile(extensionTempLibDir, EXTENSION_TEMP_LIB_PREFIX, ".jar").toFile();
temporaryFiles.add(tempLibrary);
tempLibrary.deleteOnExit();
}
catch (IOException e) {
throw new GuacamoleServerException("Temporary file "
+ "for library \"" + name + "\" bundled with "
+ "extension \"" + extension + "\" could not be "
+ "created.", e);
}
// Copy contents of bundled .jar to temporary file
try {
copyEntryToFile(extensionJar, entry, tempLibrary);
}
catch (IOException e) {
throw new GuacamoleServerException("Contents of library "
+ "\"" + name + "\" bundled with extension \""
+ extension + "\" could not be copied to a "
+ "temporary file.", e);
}
// Add temporary .jar file to classpath
urls.add(getFileURL(tempLibrary));
}
if (extensionTempLibDir != null)
logger.debug("Libraries bundled within extension \"{}\" have been "
+ "copied to temporary directory \"{}\".", extension, extensionTempLibDir);
return urls.toArray(new URL[0]);
}
catch (IOException e) {
throw new GuacamoleServerException("Contents of extension \""
+ extension + "\" cannot be read.", e);
}
}
/**
* Creates a new ExtensionClassLoader configured to load classes from the
* given extension .jar. If a necessary class cannot be found within the
* .jar, the given parent ClassLoader is used. Calling this function
* multiple times will not affect previously-returned instances of
* ExtensionClassLoader.
*
* @param extension
* The extension .jar file from which classes should be loaded.
*
* @param temporaryFiles
* A modifiable List that should be populated with all temporary files
* created for the given extension. These files should be deleted on
* application shutdown in reverse order.
*
* @param parent
* The ClassLoader to use if class resolution through the extension
* .jar fails.
*
* @throws GuacamoleException
* If the given file is not actually a file, or the contents of the
* file cannot be read.
*/
public ExtensionClassLoader(File extension, List<File> temporaryFiles,
ClassLoader parent) throws GuacamoleException {
super(getExtensionURLs(extension, temporaryFiles), null);
this.parent = parent;
}
@Override
protected Class<?> findClass(String string) throws ClassNotFoundException {
// Search only within the given URLs
try {
return super.findClass(string);
}
// Search parent classloader ONLY if not found within given URLs
catch (ClassNotFoundException e) {
return parent.loadClass(string);
}
}
}