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}