/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2008-2009 Sun Microsystems, Inc.
* Portions copyright 2012-2015 ForgeRock AS.
*/
package org.forgerock.opendj.config;
import static com.forgerock.opendj.ldap.config.AdminMessages.*;
import static com.forgerock.opendj.ldap.config.ExtensionMessages.NOTE_LOG_EXTENSION_INFORMATION;
import static com.forgerock.opendj.util.StaticUtils.EOL;
import static com.forgerock.opendj.util.StaticUtils.stackTraceToSingleLineString;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.server.config.meta.RootCfgDefn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.forgerock.opendj.ldap.config.AdminMessages;
/**
* This class is responsible for managing the configuration framework including:
* <ul>
* <li>loading core components during application initialization
* <li>loading extensions during and after application initialization
* <li>changing the property validation strategy based on whether the application is a client or server.
* </ul>
* This class defines a class loader which will be used for loading components.
* For extensions which define their own extended configuration definitions, the class loader
* will make sure that the configuration definition classes are loaded and initialized.
* <p>
* Initially the configuration framework is disabled, and calls to the {@link #getClassLoader()}
* will return the system default class loader.
* <p>
* Applications <b>MUST NOT</b> maintain persistent references to the class loader as it can change at run-time.
*/
public final class ConfigurationFramework {
/**
* Private URLClassLoader implementation. This is only required so that we can provide access to the addURL method.
*/
private static final class MyURLClassLoader extends URLClassLoader {
/**
* Create a class loader with the default parent class loader.
*/
public MyURLClassLoader() {
super(new URL[0]);
}
/**
* Create a class loader with the provided parent class loader.
*
* @param parent
* The parent class loader.
*/
public MyURLClassLoader(final ClassLoader parent) {
super(new URL[0], parent);
}
/**
* Add a Jar file to this class loader.
*
* @param jarFile
* The name of the Jar file.
* @throws MalformedURLException
* If a protocol handler for the URL could not be found, or
* if some other error occurred while constructing the URL.
* @throws SecurityException
* If a required system property value cannot be accessed.
*/
public void addJarFile(final File jarFile) throws MalformedURLException {
addURL(jarFile.toURI().toURL());
}
}
private static final String MANIFEST =
"/META-INF/services/org.forgerock.opendj.config.AbstractManagedObjectDefinition";
private static final LocalizedLogger adminLogger = LocalizedLogger
.getLocalizedLogger(AdminMessages.resourceName());
private static final Logger debugLogger = LoggerFactory.getLogger(ConfigurationFramework.class);
/** The name of the lib directory. */
private static final String LIB_DIR = "lib";
/** The name of the extensions directory. */
private static final String EXTENSIONS_DIR = "extensions";
/** The singleton instance. */
private static final ConfigurationFramework INSTANCE = new ConfigurationFramework();
/** Attribute name in jar's MANIFEST corresponding to the revision number. */
private static final String REVISION_NUMBER = "Revision-Number";
/**
* The attribute names for build information is name, version and revision number.
*/
private static final String[] BUILD_INFORMATION_ATTRIBUTE_NAMES = new String[] {
Attributes.Name.EXTENSION_NAME.toString(),
Attributes.Name.IMPLEMENTATION_VERSION.toString(), REVISION_NUMBER };
/**
* Returns the single application wide configuration framework instance.
*
* @return The single application wide configuration framework instance.
*/
public static ConfigurationFramework getInstance() {
return INSTANCE;
}
/** Set of registered Jar files. */
private Set<File> jarFiles = new HashSet<>();
/**
* Underlying class loader used to load classes and resources (null if disabled).
* <p>
* We contain a reference to the URLClassLoader rather than
* sub-class it so that it is possible to replace the loader at
* run-time. For example, when removing or replacing extension Jar
* files (the URLClassLoader only supports adding new URLs, not removal).
*/
private MyURLClassLoader loader;
private boolean isClient = true;
private String installPath;
private String instancePath;
private ClassLoader parent;
/** Private constructor. */
private ConfigurationFramework() {
// No implementation required.
}
/**
* Returns the class loader which should be used for loading classes and resources. When this configuration
* framework is disabled, the system default class loader will be returned by default.
* <p>
* Applications <b>MUST NOT</b> maintain persistent references to the class loader as it can change at run-time.
*
* @return Returns the class loader which should be used for loading classes and resources.
*/
public synchronized ClassLoader getClassLoader() {
if (loader != null) {
return loader;
} else {
return ClassLoader.getSystemClassLoader();
}
}
/**
* Initializes the configuration framework using the application's class loader as the parent class loader,
* and the current working directory as the install and instance path.
*
* @return The configuration framework.
* @throws ConfigException
* If the configuration framework could not initialize successfully.
* @throws IllegalStateException
* If the configuration framework has already been initialized.
*/
public ConfigurationFramework initialize() throws ConfigException {
return initialize(null);
}
/**
* Initializes the configuration framework using the application's class loader
* as the parent class loader, and the provided install/instance path.
*
* @param installAndInstancePath
* The path where application binaries and data are located.
* @return The configuration framework.
* @throws ConfigException
* If the configuration framework could not initialize successfully.
* @throws IllegalStateException
* If the configuration framework has already been initialized.
*/
public ConfigurationFramework initialize(final String installAndInstancePath)
throws ConfigException {
return initialize(installAndInstancePath, installAndInstancePath);
}
/**
* Initializes the configuration framework using the application's class loader
* as the parent class loader, and the provided install and instance paths.
*
* @param installPath
* The path where application binaries are located.
* @param instancePath
* The path where application data are located.
* @return The configuration framework.
* @throws ConfigException
* If the configuration framework could not initialize successfully.
* @throws IllegalStateException
* If the configuration framework has already been initialized.
*/
public ConfigurationFramework initialize(final String installPath, final String instancePath)
throws ConfigException {
return initialize(installPath, instancePath, RootCfgDefn.class.getClassLoader());
}
/**
* Initializes the configuration framework using the provided parent class
* loader and install and instance paths.
*
* @param installPath
* The path where application binaries are located.
* @param instancePath
* The path where application data are located.
* @param parent
* The parent class loader.
* @return The configuration framework.
* @throws ConfigException
* If the configuration framework could not initialize successfully.
* @throws IllegalStateException
* If the configuration framework has already been initialized.
*/
public synchronized ConfigurationFramework initialize(final String installPath,
final String instancePath, final ClassLoader parent) throws ConfigException {
if (loader != null) {
throw new IllegalStateException("configuration framework already initialized.");
}
this.installPath = installPath == null ? System.getenv("INSTALL_ROOT") : installPath;
if (instancePath != null) {
this.instancePath = instancePath;
} else {
this.instancePath = System.getenv("INSTANCE_ROOT") != null ? System.getenv("INSTANCE_ROOT")
: this.installPath;
}
this.parent = parent;
initialize0();
return this;
}
/**
* Returns {@code true} if the configuration framework is being used within
* a client application. Client applications will perform less property
* value validation than server applications because they do not have
* resources available such as the server schema.
*
* @return {@code true} if the configuration framework is being used within a client application.
*/
public boolean isClient() {
return isClient;
}
/**
* Returns {@code true} if the configuration framework has been initialized.
*
* @return {@code true} if the configuration framework has been initialized.
*/
public synchronized boolean isInitialized() {
return loader != null;
}
/**
* Prints out all information about extensions.
*
* @return A string representing all information about extensions;
* <code>null</code> if there is no information available.
*/
public String printExtensionInformation() {
final File extensionsPath = buildExtensionPath(installPath);
final List<File> extensions = new ArrayList<>();
if (extensionsPath.exists() && extensionsPath.isDirectory()) {
extensions.addAll(listFiles(extensionsPath));
}
File instanceExtensionsPath = buildExtensionPath(instancePath);
if (!extensionsPath.getAbsolutePath().equals(instanceExtensionsPath.getAbsolutePath())) {
extensions.addAll(listFiles(instanceExtensionsPath));
}
if (extensions.isEmpty()) {
return null;
}
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final PrintStream ps = new PrintStream(baos);
// prints:
// --
// Name Build number Revision number
ps.printf("--%s %-20s %-20s %-20s%s", EOL, "Name", "Build number",
"Revision number", EOL);
for (final File extension : extensions) {
printExtensionDetails(ps, extension);
}
return baos.toString();
}
private void printExtensionDetails(PrintStream ps, File extension) {
// retrieve MANIFEST entry and display name, build number and revision number
try {
JarFile jarFile = new JarFile(extension);
JarEntry entry = jarFile.getJarEntry(MANIFEST);
if (entry == null) {
return;
}
String[] information = getBuildInformation(jarFile);
ps.append("Extension: ");
boolean addBlank = false;
for (String name : information) {
if (addBlank) {
ps.append(" ");
} else {
addBlank = true;
}
ps.printf("%-20s", name);
}
ps.append(EOL);
} catch (Exception e) {
// ignore extra information for this extension
}
}
/**
* Reloads the configuration framework.
*
* @throws ConfigException
* If the configuration framework could not initialize successfully.
* @throws IllegalStateException
* If the configuration framework has not yet been initialized.
*/
public synchronized void reload() throws ConfigException {
ensureInitialized();
loader = null;
jarFiles = new HashSet<>();
initialize0();
}
/**
* Specifies whether or not the configuration framework is being used within
* a client application. Client applications will perform less property
* value validation than server applications because they do not have
* resources available such as the server schema.
*
* @param isClient
* {@code true} if the configuration framework is being used within a client application.
* @return The configuration framework.
*/
public ConfigurationFramework setIsClient(final boolean isClient) {
this.isClient = isClient;
return this;
}
private void addExtension(final List<File> extensions) throws ConfigException {
// First add the Jar files to the class loader.
final List<JarFile> jars = new LinkedList<>();
for (final File extension : extensions) {
if (jarFiles.contains(extension)) {
// Skip this file as it is already loaded.
continue;
}
// Attempt to load it.
jars.add(loadJarFile(extension));
// Register the Jar file with the class loader.
try {
loader.addJarFile(extension);
} catch (final Exception e) {
debugLogger.trace("Unable to register the jar file with the class loader", e);
final LocalizableMessage message =
ERR_ADMIN_CANNOT_OPEN_JAR_FILE.get(extension.getName(), extension
.getParent(), stackTraceToSingleLineString(e, true));
throw new ConfigException(message);
}
jarFiles.add(extension);
}
// Now forcefully load the configuration definition classes.
for (final JarFile jar : jars) {
initializeExtension(jar);
}
}
private void ensureInitialized() {
if (loader == null) {
throw new IllegalStateException("configuration framework is disabled.");
}
}
/**
* Returns a String array with the following information : <br>
* index 0: the name of the extension. <br>
* index 1: the build number of the extension. <br>
* index 2: the revision number of the extension.
*
* @param extension
* the jar file of the extension
* @return a String array containing the name, the build number and the revision number
* of the extension given in argument
* @throws java.io.IOException
* thrown if the jar file has been closed.
*/
private String[] getBuildInformation(final JarFile extension) throws IOException {
final String[] result = new String[3];
// retrieve MANIFEST entry and display name, version and revision
final Manifest manifest = extension.getManifest();
if (manifest != null) {
final Attributes attributes = manifest.getMainAttributes();
int index = 0;
for (final String name : BUILD_INFORMATION_ATTRIBUTE_NAMES) {
String value = attributes.getValue(name);
if (value == null) {
value = "<unknown>";
}
result[index++] = value;
}
}
return result;
}
private void initialize0() throws ConfigException {
if (parent != null) {
loader = new MyURLClassLoader(parent);
} else {
loader = new MyURLClassLoader();
}
// Forcefully load all configuration definition classes in OpenDS.jar.
initializeCoreComponents();
// Put extensions jars into the class loader and load all
// configuration definition classes in that they contain.
// First load the extension from the install directory, then
// from the instance directory.
File installExtensionsPath = buildExtensionPath(installPath);
File instanceExtensionsPath = buildExtensionPath(instancePath);
initializeAllExtensions(installExtensionsPath);
if (!installExtensionsPath.getAbsolutePath().equals(instanceExtensionsPath.getAbsolutePath())) {
initializeAllExtensions(instanceExtensionsPath);
}
}
private File buildExtensionPath(String directory) {
File libDir = new File(directory, LIB_DIR);
try {
return new File(libDir, EXTENSIONS_DIR).getCanonicalFile();
} catch (Exception e) {
return new File(libDir, EXTENSIONS_DIR);
}
}
/**
* Put extensions jars into the class loader and load all configuration
* definition classes in that they contain.
*
* @param extensionsPath
* Indicates where extensions are located.
* @throws ConfigException
* If the extensions folder could not be accessed or if a
* extension jar file could not be accessed or if one of the
* configuration definition classes could not be initialized.
*/
private void initializeAllExtensions(final File extensionsPath) throws ConfigException {
try {
if (!extensionsPath.exists()) {
// The extensions directory does not exist. This is not a critical problem.
adminLogger.warn(WARN_ADMIN_NO_EXTENSIONS_DIR, String.valueOf(extensionsPath));
return;
}
if (!extensionsPath.isDirectory()) {
// The extensions directory is not a directory. This is more critical.
final LocalizableMessage message =
ERR_ADMIN_EXTENSIONS_DIR_NOT_DIRECTORY.get(String.valueOf(extensionsPath));
throw new ConfigException(message);
}
// Add and initialize the extensions.
addExtension(listFiles(extensionsPath));
} catch (final ConfigException e) {
debugLogger.trace("Unable to initialize all extensions", e);
throw e;
} catch (final Exception e) {
debugLogger.trace("Unable to initialize all extensions", e);
final LocalizableMessage message =
ERR_ADMIN_EXTENSIONS_CANNOT_LIST_FILES.get(String.valueOf(extensionsPath),
stackTraceToSingleLineString(e, true));
throw new ConfigException(message, e);
}
}
private List<File> listFiles(File path) {
if (path.exists() && path.isDirectory()) {
return Arrays.asList(path.listFiles(new FileFilter() {
public boolean accept(File pathname) {
// only files with names ending with ".jar"
return pathname.isFile() && pathname.getName().endsWith(".jar");
}
}));
}
return Collections.emptyList();
}
/**
* Make sure all core configuration definitions are loaded.
*
* @throws ConfigException
* If the core manifest file could not be read or if one of the
* configuration definition classes could not be initialized.
*/
private void initializeCoreComponents() throws ConfigException {
final InputStream is = RootCfgDefn.class.getResourceAsStream(MANIFEST);
if (is == null) {
final LocalizableMessage message = ERR_ADMIN_CANNOT_FIND_CORE_MANIFEST.get(MANIFEST);
throw new ConfigException(message);
}
try {
loadDefinitionClasses(is);
} catch (final ConfigException e) {
debugLogger.trace("Unable to initialize core components", e);
final LocalizableMessage message =
ERR_CLASS_LOADER_CANNOT_LOAD_CORE.get(MANIFEST, stackTraceToSingleLineString(e,
true));
throw new ConfigException(message);
}
}
/**
* Make sure all the configuration definition classes in a extension are
* loaded.
*
* @param jarFile
* The extension's Jar file.
* @throws ConfigException
* If the extension jar file could not be accessed or if one of
* the configuration definition classes could not be
* initialized.
*/
private void initializeExtension(final JarFile jarFile) throws ConfigException {
final JarEntry entry = jarFile.getJarEntry(MANIFEST);
if (entry != null) {
InputStream is;
try {
is = jarFile.getInputStream(entry);
} catch (final Exception e) {
debugLogger.trace("Unable to get input stream from jar", e);
final LocalizableMessage message =
ERR_ADMIN_CANNOT_READ_EXTENSION_MANIFEST.get(MANIFEST, jarFile.getName(),
stackTraceToSingleLineString(e, true));
throw new ConfigException(message);
}
try {
loadDefinitionClasses(is);
} catch (final ConfigException e) {
debugLogger.trace("Unable to load classes from input stream", e);
final LocalizableMessage message =
ERR_CLASS_LOADER_CANNOT_LOAD_EXTENSION.get(jarFile.getName(), MANIFEST,
stackTraceToSingleLineString(e, true));
throw new ConfigException(message);
}
try {
// Log build information of extensions in the error log
final String[] information = getBuildInformation(jarFile);
final LocalizableMessage message =
NOTE_LOG_EXTENSION_INFORMATION.get(jarFile.getName(), information[1],
information[2]);
LocalizedLogger.getLocalizedLogger(message.resourceName()).error(message);
} catch (final Exception e) {
// Do not log information for that extension
}
}
}
/**
* Forcefully load configuration definition classes named in a manifest
* file.
*
* @param is
* The manifest file input stream.
* @throws ConfigException
* If the definition classes could not be loaded and
* initialized.
*/
private void loadDefinitionClasses(final InputStream is) throws ConfigException {
final BufferedReader reader = new BufferedReader(new InputStreamReader(is));
final List<AbstractManagedObjectDefinition<?, ?>> definitions = new LinkedList<>();
while (true) {
String className;
try {
className = reader.readLine();
} catch (final IOException e) {
final LocalizableMessage msg =
ERR_CLASS_LOADER_CANNOT_READ_MANIFEST_FILE.get(String.valueOf(e
.getMessage()));
throw new ConfigException(msg, e);
}
// Break out when the end of the manifest is reached.
if (className == null) {
break;
}
// Skip blank lines.
className = className.trim();
if (className.length() == 0) {
continue;
}
// Skip lines beginning with #.
if (className.startsWith("#")) {
continue;
}
debugLogger.trace("Loading class " + className);
// Load the class and get an instance of it if it is a definition.
Class<?> theClass;
try {
theClass = Class.forName(className, true, loader);
} catch (final Exception e) {
final LocalizableMessage msg =
ERR_CLASS_LOADER_CANNOT_LOAD_CLASS.get(className, String.valueOf(e
.getMessage()));
throw new ConfigException(msg, e);
}
if (AbstractManagedObjectDefinition.class.isAssignableFrom(theClass)) {
// We need to instantiate it using its getInstance() static
// method.
Method method;
try {
method = theClass.getMethod("getInstance");
} catch (final Exception e) {
final LocalizableMessage msg =
ERR_CLASS_LOADER_CANNOT_FIND_GET_INSTANCE_METHOD.get(className, String
.valueOf(e.getMessage()));
throw new ConfigException(msg, e);
}
// Get the definition instance.
AbstractManagedObjectDefinition<?, ?> d;
try {
d = (AbstractManagedObjectDefinition<?, ?>) method.invoke(null);
} catch (final Exception e) {
final LocalizableMessage msg =
ERR_CLASS_LOADER_CANNOT_INVOKE_GET_INSTANCE_METHOD.get(className,
String.valueOf(e.getMessage()));
throw new ConfigException(msg, e);
}
definitions.add(d);
}
}
// Initialize any definitions that were loaded.
for (final AbstractManagedObjectDefinition<?, ?> d : definitions) {
try {
d.initialize();
} catch (final Exception e) {
final LocalizableMessage msg =
ERR_CLASS_LOADER_CANNOT_INITIALIZE_DEFN.get(d.getName(), d.getClass()
.getName(), String.valueOf(e.getMessage()));
throw new ConfigException(msg, e);
}
}
}
private JarFile loadJarFile(final File jar) throws ConfigException {
try {
// Load the extension jar file.
return new JarFile(jar);
} catch (final Exception e) {
debugLogger.trace("Unable to load jar file: " + jar, e);
final LocalizableMessage message =
ERR_ADMIN_CANNOT_OPEN_JAR_FILE.get(jar.getName(), jar.getParent(),
stackTraceToSingleLineString(e, true));
throw new ConfigException(message);
}
}
/**
* Returns the installation path.
*
* @return The installation path of this instance.
*/
public String getInstallPath() {
return installPath;
}
/**
* Returns the instance path.
*
* @return The instance path.
*/
public String getInstancePath() {
return instancePath;
}
}