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 }