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