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