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