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