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