001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008-2009 Sun Microsystems, Inc.
015 * Portions copyright 2012-2016 ForgeRock AS.
016 */
017package org.forgerock.opendj.config;
018
019import static com.forgerock.opendj.ldap.config.ConfigMessages.*;
020import static com.forgerock.opendj.util.StaticUtils.*;
021
022import java.io.BufferedReader;
023import java.io.File;
024import java.io.FileFilter;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InputStreamReader;
028import java.lang.reflect.Method;
029import java.net.MalformedURLException;
030import java.net.URL;
031import java.net.URLClassLoader;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Collections;
035import java.util.HashSet;
036import java.util.LinkedList;
037import java.util.List;
038import java.util.Set;
039import java.util.jar.Attributes;
040import java.util.jar.JarEntry;
041import java.util.jar.JarFile;
042import java.util.jar.Manifest;
043
044import org.forgerock.i18n.LocalizableMessage;
045import org.forgerock.i18n.slf4j.LocalizedLogger;
046import org.forgerock.opendj.config.server.ConfigException;
047import org.forgerock.opendj.server.config.meta.RootCfgDefn;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051import com.forgerock.opendj.ldap.config.ConfigMessages;
052
053/**
054 * This class is responsible for managing the configuration framework including:
055 * <ul>
056 * <li>loading core components during application initialization
057 * <li>loading extensions during and after application initialization
058 * <li>changing the property validation strategy based on whether the application is a client or server.
059 * </ul>
060 * This class defines a class loader which will be used for loading components.
061 * For extensions which define their own extended configuration definitions, the class loader
062 * will make sure that the configuration definition classes are loaded and initialized.
063 * <p>
064 * Initially the configuration framework is disabled, and calls to the {@link #getClassLoader()}
065 * will return the system default class loader.
066 * <p>
067 * Applications <b>MUST NOT</b> maintain persistent references to the class loader as it can change at run-time.
068 */
069public final class ConfigurationFramework {
070    /**
071     * Private URLClassLoader implementation. This is only required so that we can provide access to the addURL method.
072     */
073    private static final class MyURLClassLoader extends URLClassLoader {
074
075        /** Create a class loader with the default parent class loader. */
076        public MyURLClassLoader() {
077            super(new URL[0]);
078        }
079
080        /**
081         * Create a class loader with the provided parent class loader.
082         *
083         * @param parent
084         *            The parent class loader.
085         */
086        public MyURLClassLoader(final ClassLoader parent) {
087            super(new URL[0], parent);
088        }
089
090        /**
091         * Add a Jar file to this class loader.
092         *
093         * @param jarFile
094         *            The name of the Jar file.
095         * @throws MalformedURLException
096         *             If a protocol handler for the URL could not be found, or
097         *             if some other error occurred while constructing the URL.
098         * @throws SecurityException
099         *             If a required system property value cannot be accessed.
100         */
101        public void addJarFile(final File jarFile) throws MalformedURLException {
102            addURL(jarFile.toURI().toURL());
103        }
104
105    }
106
107    /** Relative path must be used to retrieve the manifest as a jar entry from the jars. */
108    private static final String MANIFEST_RELATIVE_PATH =
109        "META-INF/services/org.forgerock.opendj.config.AbstractManagedObjectDefinition";
110
111    /** Absolute path must be used to retrieve the manifest as a resource stream. */
112    private static final String MANIFEST_ABSOLUTE_PATH = "/" + MANIFEST_RELATIVE_PATH;
113
114    private static final LocalizedLogger adminLogger = LocalizedLogger
115            .getLocalizedLogger(ConfigMessages.resourceName());
116    private static final Logger debugLogger = LoggerFactory.getLogger(ConfigurationFramework.class);
117
118    /** The name of the lib directory. */
119    private static final String LIB_DIR = "lib";
120
121    /** The name of the extensions directory. */
122    private static final String EXTENSIONS_DIR = "extensions";
123
124    /** The singleton instance. */
125    private static final ConfigurationFramework INSTANCE = new ConfigurationFramework();
126
127    /** Attribute name in jar's MANIFEST corresponding to the revision number. */
128    private static final String REVISION_NUMBER = "Revision-Number";
129
130    /** The attribute names for build information is name, version and revision number. */
131    private static final String[] BUILD_INFORMATION_ATTRIBUTE_NAMES = new String[] {
132        Attributes.Name.EXTENSION_NAME.toString(),
133        Attributes.Name.IMPLEMENTATION_VERSION.toString(), REVISION_NUMBER };
134
135    /**
136     * Returns the single application wide configuration framework instance.
137     *
138     * @return The single application wide configuration framework instance.
139     */
140    public static ConfigurationFramework getInstance() {
141        return INSTANCE;
142    }
143
144    /**
145     * Returns a string representing all information about extensions.
146     *
147     * @param installPath
148     *            The path where application binaries are located.
149     * @param instancePath
150     *            The path where application data are located.
151     *
152     * @return A string representing all information about extensions;
153     *         <code>null</code> if there is no information available.
154     */
155    public static String getPrintableExtensionInformation(final String installPath, final String instancePath) {
156        final File extensionsPath = buildExtensionDir(installPath);
157
158        final List<File> extensions = new ArrayList<>();
159
160        if (extensionsPath.exists() && extensionsPath.isDirectory()) {
161            extensions.addAll(listFiles(extensionsPath));
162        }
163
164        File instanceExtensionsPath = buildExtensionDir(instancePath);
165        if (!extensionsPath.getAbsolutePath().equals(instanceExtensionsPath.getAbsolutePath())) {
166            extensions.addAll(listFiles(instanceExtensionsPath));
167        }
168
169        if (extensions.isEmpty()) {
170            return null;
171        }
172
173        final StringBuilder sb = new StringBuilder();
174        printExtensionDetailsHeader(sb);
175
176        for (final File extension : extensions) {
177            printExtensionDetails(sb, extension);
178        }
179
180        return sb.toString();
181    }
182
183    private static void printExtensionDetailsHeader(final StringBuilder sb) {
184        // Leave space at start of the line for "Extension:"
185        sb.append("--")
186            .append(EOL)
187            .append("           Name                 Build number         Revision number")
188            .append(EOL);
189    }
190
191    private static File buildExtensionDir(String directory)  {
192        final File libDir = new File(directory, LIB_DIR);
193        final File extensionDir = new File(libDir, EXTENSIONS_DIR);
194        try {
195            return extensionDir.getCanonicalFile();
196        } catch (Exception e) {
197            return extensionDir;
198        }
199    }
200
201    private static List<File> listFiles(File path) {
202        if (path.exists() && path.isDirectory()) {
203            return Arrays.asList(path.listFiles(new FileFilter() {
204                @Override
205                public boolean accept(File pathname) {
206                    // only files with names ending with ".jar"
207                    return pathname.isFile() && pathname.getName().endsWith(".jar");
208                }
209            }));
210        }
211        return Collections.emptyList();
212    }
213
214    private static void printExtensionDetails(final StringBuilder sb, final File extension) {
215        // retrieve MANIFEST entry and display name, build number and revision number
216        try (JarFile jarFile = new JarFile(extension)) {
217            JarEntry entry = jarFile.getJarEntry(MANIFEST_RELATIVE_PATH);
218            if (entry == null) {
219                return;
220            }
221
222            String[] information = getBuildInformation(jarFile);
223            sb.append("Extension:");
224            for (final String name : information) {
225                sb.append(" ").append(String.format("%-20s", name));
226            }
227            sb.append(EOL);
228        } catch (final IOException ignored) {
229            // ignore extra information for this extension
230        }
231    }
232
233    /**
234     * Returns a String array with the following information : <br>
235     * index 0: the name of the extension. <br>
236     * index 1: the build number of the extension. <br>
237     * index 2: the revision number of the extension.
238     *
239     * @param extension
240     *            the jar file of the extension
241     * @return a String array containing the name, the build number and the revision number
242     *            of the extension given in argument
243     * @throws java.io.IOException
244     *             thrown if the jar file has been closed.
245     */
246    private static String[] getBuildInformation(final JarFile extension) throws IOException {
247        final String[] result = new String[3];
248
249        final Manifest manifest = extension.getManifest();
250        if (manifest != null) {
251            final Attributes attributes = manifest.getMainAttributes();
252
253            int index = 0;
254            for (final String name : BUILD_INFORMATION_ATTRIBUTE_NAMES) {
255                String value = attributes.getValue(name);
256                if (value == null) {
257                    value = "<unknown>";
258                }
259                result[index++] = value;
260            }
261        }
262
263        return result;
264    }
265
266    /** Set of registered Jar files. */
267    private Set<File> jarFiles = new HashSet<>();
268
269    /**
270     * Underlying class loader used to load classes and resources (null if disabled).
271     * <p>
272     * We contain a reference to the URLClassLoader rather than
273     * sub-class it so that it is possible to replace the loader at
274     * run-time. For example, when removing or replacing extension Jar
275     * files (the URLClassLoader only supports adding new URLs, not removal).
276     */
277    private MyURLClassLoader loader;
278
279    private boolean isClient = true;
280    private String installPath;
281    private String instancePath;
282    private ClassLoader parent;
283
284    /** Private constructor. */
285    private ConfigurationFramework() {
286        // No implementation required.
287    }
288
289    /**
290     * Returns the class loader which should be used for loading classes and resources. When this configuration
291     * framework is disabled, the system default class loader will be returned by default.
292     * <p>
293     * Applications <b>MUST NOT</b> maintain persistent references to the class loader as it can change at run-time.
294     *
295     * @return Returns the class loader which should be used for loading classes and resources.
296     */
297    public synchronized ClassLoader getClassLoader() {
298        if (loader != null) {
299            return loader;
300        } else {
301            return ClassLoader.getSystemClassLoader();
302        }
303    }
304
305    /**
306     * Initializes the configuration framework using the application's class loader as the parent class loader,
307     * and the current working directory as the install and instance path.
308     *
309     * @return The configuration framework.
310     * @throws ConfigException
311     *             If the configuration framework could not initialize successfully.
312     * @throws IllegalStateException
313     *             If the configuration framework has already been initialized.
314     */
315    public ConfigurationFramework initialize() throws ConfigException {
316        return initialize(null);
317    }
318
319    /**
320     * Initializes the configuration framework using the application's class loader
321     * as the parent class loader, and the provided install/instance path.
322     *
323     * @param installAndInstancePath
324     *            The path where application binaries and data are located.
325     * @return The configuration framework.
326     * @throws ConfigException
327     *             If the configuration framework could not initialize successfully.
328     * @throws IllegalStateException
329     *             If the configuration framework has already been initialized.
330     */
331    public ConfigurationFramework initialize(final String installAndInstancePath)
332            throws ConfigException {
333        return initialize(installAndInstancePath, installAndInstancePath);
334    }
335
336    /**
337     * Initializes the configuration framework using the application's class loader
338     * as the parent class loader, and the provided install and instance paths.
339     *
340     * @param installPath
341     *            The path where application binaries are located.
342     * @param instancePath
343     *            The path where application data are located.
344     * @return The configuration framework.
345     * @throws ConfigException
346     *             If the configuration framework could not initialize successfully.
347     * @throws IllegalStateException
348     *             If the configuration framework has already been initialized.
349     */
350    public ConfigurationFramework initialize(final String installPath, final String instancePath)
351            throws ConfigException {
352        return initialize(installPath, instancePath, RootCfgDefn.class.getClassLoader());
353    }
354
355    /**
356     * Initializes the configuration framework using the provided parent class
357     * loader and install and instance paths.
358     *
359     * @param installPath
360     *            The path where application binaries are located.
361     * @param instancePath
362     *            The path where application data are located.
363     * @param parent
364     *            The parent class loader.
365     * @return The configuration framework.
366     * @throws ConfigException
367     *             If the configuration framework could not initialize successfully.
368     * @throws IllegalStateException
369     *             If the configuration framework has already been initialized.
370     */
371    public synchronized ConfigurationFramework initialize(final String installPath,
372            final String instancePath, final ClassLoader parent) throws ConfigException {
373        if (loader != null) {
374            throw new IllegalStateException("configuration framework already initialized.");
375        }
376        this.installPath = installPath != null ? installPath : System.getenv("INSTALL_ROOT");
377        if (instancePath != null) {
378            this.instancePath = instancePath;
379        } else {
380            String instanceRoot = System.getenv("INSTANCE_ROOT");
381            this.instancePath = instanceRoot != null ? instanceRoot : this.installPath;
382        }
383        this.parent = parent;
384        initialize0();
385        return this;
386    }
387
388    /**
389     * Returns {@code true} if the configuration framework is being used within
390     * a client application. Client applications will perform less property
391     * value validation than server applications because they do not have
392     * resources available such as the server schema.
393     *
394     * @return {@code true} if the configuration framework is being used within a client application.
395     */
396    public boolean isClient() {
397        return isClient;
398    }
399
400    /**
401     * Returns {@code true} if the configuration framework has been initialized.
402     *
403     * @return {@code true} if the configuration framework has been initialized.
404     */
405    public synchronized boolean isInitialized() {
406        return loader != null;
407    }
408
409    /**
410     * Reloads the configuration framework.
411     *
412     * @throws ConfigException
413     *             If the configuration framework could not initialize successfully.
414     * @throws IllegalStateException
415     *             If the configuration framework has not yet been initialized.
416     */
417    public synchronized void reload() throws ConfigException {
418        ensureInitialized();
419        loader = null;
420        jarFiles = new HashSet<>();
421        initialize0();
422    }
423
424    /**
425     * Specifies whether the configuration framework is being used within
426     * a client application. Client applications will perform less property
427     * value validation than server applications because they do not have
428     * resources available such as the server schema.
429     *
430     * @param isClient
431     *            {@code true} if the configuration framework is being used within a client application.
432     * @return The configuration framework.
433     */
434    public ConfigurationFramework setIsClient(final boolean isClient) {
435        this.isClient = isClient;
436        return this;
437    }
438
439    private void addExtension(final List<File> extensions) throws ConfigException {
440        // First add the Jar files to the class loader.
441        final List<JarFile> jars = new LinkedList<>();
442        for (final File extension : extensions) {
443            if (jarFiles.contains(extension)) {
444                // Skip this file as it is already loaded.
445                continue;
446            }
447
448            // Attempt to load it.
449            jars.add(loadJarFile(extension));
450
451            // Register the Jar file with the class loader.
452            try {
453                loader.addJarFile(extension);
454            } catch (final Exception e) {
455                debugLogger.trace("Unable to register the jar file with the class loader", e);
456                final LocalizableMessage message =
457                        ERR_ADMIN_CANNOT_OPEN_JAR_FILE.get(extension.getName(), extension
458                                .getParent(), stackTraceToSingleLineString(e, true));
459                throw new ConfigException(message);
460            }
461            jarFiles.add(extension);
462        }
463
464        // Now forcefully load the configuration definition classes.
465        for (final JarFile jar : jars) {
466            initializeExtension(jar);
467        }
468    }
469
470    private void ensureInitialized() {
471        if (loader == null) {
472            throw new IllegalStateException("configuration framework is disabled.");
473        }
474    }
475
476    private void initialize0() throws ConfigException {
477        if (parent != null) {
478            loader = new MyURLClassLoader(parent);
479        } else {
480            loader = new MyURLClassLoader();
481        }
482
483        // Forcefully load all configuration definition classes in OpenDS.jar.
484        initializeCoreComponents();
485
486        // Put extensions jars into the class loader and load all
487        // configuration definition classes in that they contain.
488        // First load the extension from the install directory, then
489        // from the instance directory.
490        File installExtensionsPath  = buildExtensionDir(installPath);
491        File instanceExtensionsPath = buildExtensionDir(instancePath);
492
493        initializeAllExtensions(installExtensionsPath);
494
495        if (!installExtensionsPath.getAbsolutePath().equals(instanceExtensionsPath.getAbsolutePath())) {
496            initializeAllExtensions(instanceExtensionsPath);
497        }
498    }
499
500    /**
501     * Put extensions jars into the class loader and load all configuration
502     * definition classes in that they contain.
503     *
504     * @param extensionsPath
505     *            Indicates where extensions are located.
506     * @throws ConfigException
507     *             If the extensions folder could not be accessed or if a
508     *             extension jar file could not be accessed or if one of the
509     *             configuration definition classes could not be initialized.
510     */
511    private void initializeAllExtensions(final File extensionsPath) throws ConfigException {
512        try {
513            if (!extensionsPath.exists()) {
514                // The extensions directory does not exist. This is not a critical problem.
515                adminLogger.warn(WARN_ADMIN_NO_EXTENSIONS_DIR, extensionsPath);
516                return;
517            }
518
519            if (!extensionsPath.isDirectory()) {
520                // The extensions directory is not a directory. This is more critical.
521                throw new ConfigException(ERR_ADMIN_EXTENSIONS_DIR_NOT_DIRECTORY.get(extensionsPath));
522            }
523
524            // Add and initialize the extensions.
525            addExtension(listFiles(extensionsPath));
526        } catch (final ConfigException e) {
527            debugLogger.trace("Unable to initialize all extensions", e);
528            throw e;
529        } catch (final Exception e) {
530            debugLogger.trace("Unable to initialize all extensions", e);
531            final LocalizableMessage message = ERR_ADMIN_EXTENSIONS_CANNOT_LIST_FILES.get(
532                extensionsPath, stackTraceToSingleLineString(e, true));
533            throw new ConfigException(message, e);
534        }
535    }
536
537    /**
538     * Make sure all core configuration definitions are loaded.
539     *
540     * @throws ConfigException
541     *             If the core manifest file could not be read or if one of the
542     *             configuration definition classes could not be initialized.
543     */
544    private void initializeCoreComponents() throws ConfigException {
545        final InputStream is = RootCfgDefn.class.getResourceAsStream(MANIFEST_ABSOLUTE_PATH);
546        if (is == null) {
547            throw new ConfigException(ERR_ADMIN_CANNOT_FIND_CORE_MANIFEST.get(MANIFEST_ABSOLUTE_PATH));
548        }
549        try {
550            loadDefinitionClasses(is);
551        } catch (final ConfigException e) {
552            debugLogger.trace("Unable to initialize core components", e);
553            throw new ConfigException(ERR_CLASS_LOADER_CANNOT_LOAD_CORE.get(
554                MANIFEST_ABSOLUTE_PATH, stackTraceToSingleLineString(e, true)));
555        }
556    }
557
558    /**
559     * Make sure all the configuration definition classes in a extension are
560     * loaded.
561     *
562     * @param jarFile
563     *            The extension's Jar file.
564     * @throws ConfigException
565     *             If the extension jar file could not be accessed or if one of
566     *             the configuration definition classes could not be
567     *             initialized.
568     */
569    private void initializeExtension(final JarFile jarFile) throws ConfigException {
570        final JarEntry entry = jarFile.getJarEntry(MANIFEST_RELATIVE_PATH);
571        if (entry != null) {
572            InputStream is;
573            try {
574                is = jarFile.getInputStream(entry);
575            } catch (final Exception e) {
576                debugLogger.trace("Unable to get input stream from jar", e);
577                final LocalizableMessage message =
578                        ERR_ADMIN_CANNOT_READ_EXTENSION_MANIFEST.get(MANIFEST_RELATIVE_PATH, jarFile.getName(),
579                                stackTraceToSingleLineString(e, true));
580                throw new ConfigException(message);
581            }
582
583            try {
584                loadDefinitionClasses(is);
585            } catch (final ConfigException e) {
586                debugLogger.trace("Unable to load classes from input stream", e);
587                final LocalizableMessage message = ERR_CLASS_LOADER_CANNOT_LOAD_EXTENSION.get(
588                    jarFile.getName(), MANIFEST_RELATIVE_PATH, stackTraceToSingleLineString(e, true));
589                throw new ConfigException(message);
590            }
591            try {
592                // Log build information of extensions in the error log
593                final String[] information = getBuildInformation(jarFile);
594                final LocalizableMessage message = NOTE_LOG_EXTENSION_INFORMATION.get(
595                    jarFile.getName(), information[1], information[2]);
596                LocalizedLogger.getLocalizedLogger(message.resourceName()).info(message);
597            } catch (final Exception e) {
598                // Do not log information for that extension
599            }
600        }
601    }
602
603    /**
604     * Forcefully load configuration definition classes named in a manifest file.
605     *
606     * @param is
607     *            The manifest file input stream.
608     * @throws ConfigException
609     *             If the definition classes could not be loaded and initialized.
610     */
611    private void loadDefinitionClasses(final InputStream is) throws ConfigException {
612        // Cannot use ServiceLoader because constructors are private
613        final BufferedReader reader = new BufferedReader(new InputStreamReader(is));
614        final List<AbstractManagedObjectDefinition<?, ?>> definitions = new LinkedList<>();
615        while (true) {
616            String className;
617            try {
618                className = reader.readLine();
619            } catch (final IOException e) {
620                final LocalizableMessage msg =
621                        ERR_CLASS_LOADER_CANNOT_READ_MANIFEST_FILE.get(e.getMessage());
622                throw new ConfigException(msg, e);
623            }
624
625            // Break out when the end of the manifest is reached.
626            if (className == null) {
627                break;
628            }
629
630            className = className.trim();
631            // Skip blank lines or lines beginning with #.
632            if (className.isEmpty() || className.startsWith("#")) {
633                continue;
634            }
635
636            debugLogger.trace("Loading class " + className);
637
638            // Load the class and get an instance of it if it is a definition.
639            Class<?> theClass;
640            try {
641                theClass = Class.forName(className, true, loader);
642            } catch (final Exception e) {
643                final LocalizableMessage msg =
644                        ERR_CLASS_LOADER_CANNOT_LOAD_CLASS.get(className, e.getMessage());
645                throw new ConfigException(msg, e);
646            }
647            if (AbstractManagedObjectDefinition.class.isAssignableFrom(theClass)) {
648                // We need to instantiate it using its getInstance() static method.
649                Method method;
650                try {
651                    method = theClass.getMethod("getInstance");
652                } catch (final Exception e) {
653                    final LocalizableMessage msg =
654                            ERR_CLASS_LOADER_CANNOT_FIND_GET_INSTANCE_METHOD.get(className, e.getMessage());
655                    throw new ConfigException(msg, e);
656                }
657
658                // Get the definition instance.
659                AbstractManagedObjectDefinition<?, ?> d;
660                try {
661                    d = (AbstractManagedObjectDefinition<?, ?>) method.invoke(null);
662                } catch (final Exception e) {
663                    final LocalizableMessage msg =
664                            ERR_CLASS_LOADER_CANNOT_INVOKE_GET_INSTANCE_METHOD.get(className, e.getMessage());
665                    throw new ConfigException(msg, e);
666                }
667                definitions.add(d);
668            }
669        }
670
671        // Initialize any definitions that were loaded.
672        for (final AbstractManagedObjectDefinition<?, ?> d : definitions) {
673            try {
674                d.initialize();
675            } catch (final Exception e) {
676                final LocalizableMessage msg = ERR_CLASS_LOADER_CANNOT_INITIALIZE_DEFN.get(
677                    d.getName(), d.getClass().getName(), e.getMessage());
678                throw new ConfigException(msg, e);
679            }
680        }
681    }
682
683    private JarFile loadJarFile(final File jar) throws ConfigException {
684        try {
685            // Load the extension jar file.
686            return new JarFile(jar);
687        } catch (final Exception e) {
688            debugLogger.trace("Unable to load jar file: " + jar, e);
689
690            final LocalizableMessage message =
691                    ERR_ADMIN_CANNOT_OPEN_JAR_FILE.get(jar.getName(), jar.getParent(),
692                            stackTraceToSingleLineString(e, true));
693            throw new ConfigException(message);
694        }
695    }
696
697    /**
698     * Returns the installation path.
699     *
700     * @return The installation path of this instance.
701     */
702    public String getInstallPath() {
703        return installPath;
704    }
705
706    /**
707     * Returns the instance path.
708     *
709     * @return The instance path.
710     */
711    public String getInstancePath() {
712        return instancePath;
713    }
714}