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