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}