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-2016 ForgeRock AS. 016 */ 017package org.forgerock.opendj.config; 018 019import static com.forgerock.opendj.ldap.config.ConfigMessages.*; 020import static com.forgerock.opendj.util.StaticUtils.*; 021 022import java.io.BufferedReader; 023import java.io.File; 024import java.io.FileFilter; 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.InputStreamReader; 028import java.lang.reflect.Method; 029import java.net.MalformedURLException; 030import java.net.URL; 031import java.net.URLClassLoader; 032import java.util.ArrayList; 033import java.util.Arrays; 034import java.util.Collections; 035import java.util.HashSet; 036import java.util.LinkedList; 037import java.util.List; 038import java.util.Set; 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.forgerock.i18n.slf4j.LocalizedLogger; 046import org.forgerock.opendj.config.server.ConfigException; 047import org.forgerock.opendj.server.config.meta.RootCfgDefn; 048import org.slf4j.Logger; 049import org.slf4j.LoggerFactory; 050 051import com.forgerock.opendj.ldap.config.ConfigMessages; 052 053/** 054 * This class is responsible for managing the configuration framework including: 055 * <ul> 056 * <li>loading core components during application initialization 057 * <li>loading extensions during and after application initialization 058 * <li>changing the property validation strategy based on whether the application is a client or server. 059 * </ul> 060 * This class defines a class loader which will be used for loading components. 061 * For extensions which define their own extended configuration definitions, the class loader 062 * will make sure that the configuration definition classes are loaded and initialized. 063 * <p> 064 * Initially the configuration framework is disabled, and calls to the {@link #getClassLoader()} 065 * will return the system default class loader. 066 * <p> 067 * Applications <b>MUST NOT</b> maintain persistent references to the class loader as it can change at run-time. 068 */ 069public final class ConfigurationFramework { 070 /** 071 * Private URLClassLoader implementation. This is only required so that we can provide access to the addURL method. 072 */ 073 private static final class MyURLClassLoader extends URLClassLoader { 074 075 /** Create a class loader with the default parent class loader. */ 076 public MyURLClassLoader() { 077 super(new URL[0]); 078 } 079 080 /** 081 * Create a class loader with the provided parent class loader. 082 * 083 * @param parent 084 * The parent class loader. 085 */ 086 public MyURLClassLoader(final ClassLoader parent) { 087 super(new URL[0], parent); 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 097 * if some other error occurred while constructing the URL. 098 * @throws SecurityException 099 * If a required system property value cannot be accessed. 100 */ 101 public void addJarFile(final File jarFile) throws MalformedURLException { 102 addURL(jarFile.toURI().toURL()); 103 } 104 105 } 106 107 /** Relative path must be used to retrieve the manifest as a jar entry from the jars. */ 108 private static final String MANIFEST_RELATIVE_PATH = 109 "META-INF/services/org.forgerock.opendj.config.AbstractManagedObjectDefinition"; 110 111 /** Absolute path must be used to retrieve the manifest as a resource stream. */ 112 private static final String MANIFEST_ABSOLUTE_PATH = "/" + MANIFEST_RELATIVE_PATH; 113 114 private static final LocalizedLogger adminLogger = LocalizedLogger 115 .getLocalizedLogger(ConfigMessages.resourceName()); 116 private static final Logger debugLogger = LoggerFactory.getLogger(ConfigurationFramework.class); 117 118 /** The name of the lib directory. */ 119 private static final String LIB_DIR = "lib"; 120 121 /** The name of the extensions directory. */ 122 private static final String EXTENSIONS_DIR = "extensions"; 123 124 /** The singleton instance. */ 125 private static final ConfigurationFramework INSTANCE = new ConfigurationFramework(); 126 127 /** Attribute name in jar's MANIFEST corresponding to the revision number. */ 128 private static final String REVISION_NUMBER = "Revision-Number"; 129 130 /** The attribute names for build information is name, version and revision number. */ 131 private static final String[] BUILD_INFORMATION_ATTRIBUTE_NAMES = new String[] { 132 Attributes.Name.EXTENSION_NAME.toString(), 133 Attributes.Name.IMPLEMENTATION_VERSION.toString(), REVISION_NUMBER }; 134 135 /** 136 * Returns the single application wide configuration framework instance. 137 * 138 * @return The single application wide configuration framework instance. 139 */ 140 public static ConfigurationFramework getInstance() { 141 return INSTANCE; 142 } 143 144 /** 145 * Returns a string representing all information about extensions. 146 * 147 * @param installPath 148 * The path where application binaries are located. 149 * @param instancePath 150 * The path where application data are located. 151 * 152 * @return A string representing all information about extensions; 153 * <code>null</code> if there is no information available. 154 */ 155 public static String getPrintableExtensionInformation(final String installPath, final String instancePath) { 156 final File extensionsPath = buildExtensionDir(installPath); 157 158 final List<File> extensions = new ArrayList<>(); 159 160 if (extensionsPath.exists() && extensionsPath.isDirectory()) { 161 extensions.addAll(listFiles(extensionsPath)); 162 } 163 164 File instanceExtensionsPath = buildExtensionDir(instancePath); 165 if (!extensionsPath.getAbsolutePath().equals(instanceExtensionsPath.getAbsolutePath())) { 166 extensions.addAll(listFiles(instanceExtensionsPath)); 167 } 168 169 if (extensions.isEmpty()) { 170 return null; 171 } 172 173 final StringBuilder sb = new StringBuilder(); 174 printExtensionDetailsHeader(sb); 175 176 for (final File extension : extensions) { 177 printExtensionDetails(sb, extension); 178 } 179 180 return sb.toString(); 181 } 182 183 private static void printExtensionDetailsHeader(final StringBuilder sb) { 184 // Leave space at start of the line for "Extension:" 185 sb.append("--") 186 .append(EOL) 187 .append(" Name Build number Revision number") 188 .append(EOL); 189 } 190 191 private static File buildExtensionDir(String directory) { 192 final File libDir = new File(directory, LIB_DIR); 193 final File extensionDir = new File(libDir, EXTENSIONS_DIR); 194 try { 195 return extensionDir.getCanonicalFile(); 196 } catch (Exception e) { 197 return extensionDir; 198 } 199 } 200 201 private static List<File> listFiles(File path) { 202 if (path.exists() && path.isDirectory()) { 203 return Arrays.asList(path.listFiles(new FileFilter() { 204 @Override 205 public boolean accept(File pathname) { 206 // only files with names ending with ".jar" 207 return pathname.isFile() && pathname.getName().endsWith(".jar"); 208 } 209 })); 210 } 211 return Collections.emptyList(); 212 } 213 214 private static void printExtensionDetails(final StringBuilder sb, final File extension) { 215 // retrieve MANIFEST entry and display name, build number and revision number 216 try (JarFile jarFile = new JarFile(extension)) { 217 JarEntry entry = jarFile.getJarEntry(MANIFEST_RELATIVE_PATH); 218 if (entry == null) { 219 return; 220 } 221 222 String[] information = getBuildInformation(jarFile); 223 sb.append("Extension:"); 224 for (final String name : information) { 225 sb.append(" ").append(String.format("%-20s", name)); 226 } 227 sb.append(EOL); 228 } catch (final IOException ignored) { 229 // ignore extra information for this extension 230 } 231 } 232 233 /** 234 * Returns a String array with the following information : <br> 235 * index 0: the name of the extension. <br> 236 * index 1: the build number of the extension. <br> 237 * index 2: the revision number of the extension. 238 * 239 * @param extension 240 * the jar file of the extension 241 * @return a String array containing the name, the build number and the revision number 242 * of the extension given in argument 243 * @throws java.io.IOException 244 * thrown if the jar file has been closed. 245 */ 246 private static String[] getBuildInformation(final JarFile extension) throws IOException { 247 final String[] result = new String[3]; 248 249 final Manifest manifest = extension.getManifest(); 250 if (manifest != null) { 251 final Attributes attributes = manifest.getMainAttributes(); 252 253 int index = 0; 254 for (final String name : BUILD_INFORMATION_ATTRIBUTE_NAMES) { 255 String value = attributes.getValue(name); 256 if (value == null) { 257 value = "<unknown>"; 258 } 259 result[index++] = value; 260 } 261 } 262 263 return result; 264 } 265 266 /** Set of registered Jar files. */ 267 private Set<File> jarFiles = new HashSet<>(); 268 269 /** 270 * Underlying class loader used to load classes and resources (null if disabled). 271 * <p> 272 * We contain a reference to the URLClassLoader rather than 273 * sub-class it so that it is possible to replace the loader at 274 * run-time. For example, when removing or replacing extension Jar 275 * files (the URLClassLoader only supports adding new URLs, not removal). 276 */ 277 private MyURLClassLoader loader; 278 279 private boolean isClient = true; 280 private String installPath; 281 private String instancePath; 282 private ClassLoader parent; 283 284 /** Private constructor. */ 285 private ConfigurationFramework() { 286 // No implementation required. 287 } 288 289 /** 290 * Returns the class loader which should be used for loading classes and resources. When this configuration 291 * framework is disabled, the system default class loader will be returned by default. 292 * <p> 293 * Applications <b>MUST NOT</b> maintain persistent references to the class loader as it can change at run-time. 294 * 295 * @return Returns the class loader which should be used for loading classes and resources. 296 */ 297 public synchronized ClassLoader getClassLoader() { 298 if (loader != null) { 299 return loader; 300 } else { 301 return ClassLoader.getSystemClassLoader(); 302 } 303 } 304 305 /** 306 * Initializes the configuration framework using the application's class loader as the parent class loader, 307 * and the current working directory as the install and instance path. 308 * 309 * @return The configuration framework. 310 * @throws ConfigException 311 * If the configuration framework could not initialize successfully. 312 * @throws IllegalStateException 313 * If the configuration framework has already been initialized. 314 */ 315 public ConfigurationFramework initialize() throws ConfigException { 316 return initialize(null); 317 } 318 319 /** 320 * Initializes the configuration framework using the application's class loader 321 * as the parent class loader, and the provided install/instance path. 322 * 323 * @param installAndInstancePath 324 * The path where application binaries and data are located. 325 * @return The configuration framework. 326 * @throws ConfigException 327 * If the configuration framework could not initialize successfully. 328 * @throws IllegalStateException 329 * If the configuration framework has already been initialized. 330 */ 331 public ConfigurationFramework initialize(final String installAndInstancePath) 332 throws ConfigException { 333 return initialize(installAndInstancePath, installAndInstancePath); 334 } 335 336 /** 337 * Initializes the configuration framework using the application's class loader 338 * as the parent class loader, and the provided install and instance paths. 339 * 340 * @param installPath 341 * The path where application binaries are located. 342 * @param instancePath 343 * The path where application data are located. 344 * @return The configuration framework. 345 * @throws ConfigException 346 * If the configuration framework could not initialize successfully. 347 * @throws IllegalStateException 348 * If the configuration framework has already been initialized. 349 */ 350 public ConfigurationFramework initialize(final String installPath, final String instancePath) 351 throws ConfigException { 352 return initialize(installPath, instancePath, RootCfgDefn.class.getClassLoader()); 353 } 354 355 /** 356 * Initializes the configuration framework using the provided parent class 357 * loader and install and instance paths. 358 * 359 * @param installPath 360 * The path where application binaries are located. 361 * @param instancePath 362 * The path where application data are located. 363 * @param parent 364 * The parent class loader. 365 * @return The configuration framework. 366 * @throws ConfigException 367 * If the configuration framework could not initialize successfully. 368 * @throws IllegalStateException 369 * If the configuration framework has already been initialized. 370 */ 371 public synchronized ConfigurationFramework initialize(final String installPath, 372 final String instancePath, final ClassLoader parent) throws ConfigException { 373 if (loader != null) { 374 throw new IllegalStateException("configuration framework already initialized."); 375 } 376 this.installPath = installPath != null ? installPath : System.getenv("INSTALL_ROOT"); 377 if (instancePath != null) { 378 this.instancePath = instancePath; 379 } else { 380 String instanceRoot = System.getenv("INSTANCE_ROOT"); 381 this.instancePath = instanceRoot != null ? instanceRoot : this.installPath; 382 } 383 this.parent = parent; 384 initialize0(); 385 return this; 386 } 387 388 /** 389 * Returns {@code true} if the configuration framework is being used within 390 * a client application. Client applications will perform less property 391 * value validation than server applications because they do not have 392 * resources available such as the server schema. 393 * 394 * @return {@code true} if the configuration framework is being used within a client application. 395 */ 396 public boolean isClient() { 397 return isClient; 398 } 399 400 /** 401 * Returns {@code true} if the configuration framework has been initialized. 402 * 403 * @return {@code true} if the configuration framework has been initialized. 404 */ 405 public synchronized boolean isInitialized() { 406 return loader != null; 407 } 408 409 /** 410 * Reloads the configuration framework. 411 * 412 * @throws ConfigException 413 * If the configuration framework could not initialize successfully. 414 * @throws IllegalStateException 415 * If the configuration framework has not yet been initialized. 416 */ 417 public synchronized void reload() throws ConfigException { 418 ensureInitialized(); 419 loader = null; 420 jarFiles = new HashSet<>(); 421 initialize0(); 422 } 423 424 /** 425 * Specifies whether the configuration framework is being used within 426 * a client application. Client applications will perform less property 427 * value validation than server applications because they do not have 428 * resources available such as the server schema. 429 * 430 * @param isClient 431 * {@code true} if the configuration framework is being used within a client application. 432 * @return The configuration framework. 433 */ 434 public ConfigurationFramework setIsClient(final boolean isClient) { 435 this.isClient = isClient; 436 return this; 437 } 438 439 private void addExtension(final List<File> extensions) throws ConfigException { 440 // First add the Jar files to the class loader. 441 final List<JarFile> jars = new LinkedList<>(); 442 for (final File extension : extensions) { 443 if (jarFiles.contains(extension)) { 444 // Skip this file as it is already loaded. 445 continue; 446 } 447 448 // Attempt to load it. 449 jars.add(loadJarFile(extension)); 450 451 // Register the Jar file with the class loader. 452 try { 453 loader.addJarFile(extension); 454 } catch (final Exception e) { 455 debugLogger.trace("Unable to register the jar file with the class loader", e); 456 final LocalizableMessage message = 457 ERR_ADMIN_CANNOT_OPEN_JAR_FILE.get(extension.getName(), extension 458 .getParent(), stackTraceToSingleLineString(e, true)); 459 throw new ConfigException(message); 460 } 461 jarFiles.add(extension); 462 } 463 464 // Now forcefully load the configuration definition classes. 465 for (final JarFile jar : jars) { 466 initializeExtension(jar); 467 } 468 } 469 470 private void ensureInitialized() { 471 if (loader == null) { 472 throw new IllegalStateException("configuration framework is disabled."); 473 } 474 } 475 476 private void initialize0() throws ConfigException { 477 if (parent != null) { 478 loader = new MyURLClassLoader(parent); 479 } else { 480 loader = new MyURLClassLoader(); 481 } 482 483 // Forcefully load all configuration definition classes in OpenDS.jar. 484 initializeCoreComponents(); 485 486 // Put extensions jars into the class loader and load all 487 // configuration definition classes in that they contain. 488 // First load the extension from the install directory, then 489 // from the instance directory. 490 File installExtensionsPath = buildExtensionDir(installPath); 491 File instanceExtensionsPath = buildExtensionDir(instancePath); 492 493 initializeAllExtensions(installExtensionsPath); 494 495 if (!installExtensionsPath.getAbsolutePath().equals(instanceExtensionsPath.getAbsolutePath())) { 496 initializeAllExtensions(instanceExtensionsPath); 497 } 498 } 499 500 /** 501 * Put extensions jars into the class loader and load all configuration 502 * definition classes in that they contain. 503 * 504 * @param extensionsPath 505 * Indicates where extensions are located. 506 * @throws ConfigException 507 * If the extensions folder could not be accessed or if a 508 * extension jar file could not be accessed or if one of the 509 * configuration definition classes could not be initialized. 510 */ 511 private void initializeAllExtensions(final File extensionsPath) throws ConfigException { 512 try { 513 if (!extensionsPath.exists()) { 514 // The extensions directory does not exist. This is not a critical problem. 515 adminLogger.warn(WARN_ADMIN_NO_EXTENSIONS_DIR, extensionsPath); 516 return; 517 } 518 519 if (!extensionsPath.isDirectory()) { 520 // The extensions directory is not a directory. This is more critical. 521 throw new ConfigException(ERR_ADMIN_EXTENSIONS_DIR_NOT_DIRECTORY.get(extensionsPath)); 522 } 523 524 // Add and initialize the extensions. 525 addExtension(listFiles(extensionsPath)); 526 } catch (final ConfigException e) { 527 debugLogger.trace("Unable to initialize all extensions", e); 528 throw e; 529 } catch (final Exception e) { 530 debugLogger.trace("Unable to initialize all extensions", e); 531 final LocalizableMessage message = ERR_ADMIN_EXTENSIONS_CANNOT_LIST_FILES.get( 532 extensionsPath, stackTraceToSingleLineString(e, true)); 533 throw new ConfigException(message, e); 534 } 535 } 536 537 /** 538 * Make sure all core configuration definitions are loaded. 539 * 540 * @throws ConfigException 541 * If the core manifest file could not be read or if one of the 542 * configuration definition classes could not be initialized. 543 */ 544 private void initializeCoreComponents() throws ConfigException { 545 final InputStream is = RootCfgDefn.class.getResourceAsStream(MANIFEST_ABSOLUTE_PATH); 546 if (is == null) { 547 throw new ConfigException(ERR_ADMIN_CANNOT_FIND_CORE_MANIFEST.get(MANIFEST_ABSOLUTE_PATH)); 548 } 549 try { 550 loadDefinitionClasses(is); 551 } catch (final ConfigException e) { 552 debugLogger.trace("Unable to initialize core components", e); 553 throw new ConfigException(ERR_CLASS_LOADER_CANNOT_LOAD_CORE.get( 554 MANIFEST_ABSOLUTE_PATH, stackTraceToSingleLineString(e, true))); 555 } 556 } 557 558 /** 559 * Make sure all the configuration definition classes in a extension are 560 * loaded. 561 * 562 * @param jarFile 563 * The extension's Jar file. 564 * @throws ConfigException 565 * If the extension jar file could not be accessed or if one of 566 * the configuration definition classes could not be 567 * initialized. 568 */ 569 private void initializeExtension(final JarFile jarFile) throws ConfigException { 570 final JarEntry entry = jarFile.getJarEntry(MANIFEST_RELATIVE_PATH); 571 if (entry != null) { 572 InputStream is; 573 try { 574 is = jarFile.getInputStream(entry); 575 } catch (final Exception e) { 576 debugLogger.trace("Unable to get input stream from jar", e); 577 final LocalizableMessage message = 578 ERR_ADMIN_CANNOT_READ_EXTENSION_MANIFEST.get(MANIFEST_RELATIVE_PATH, jarFile.getName(), 579 stackTraceToSingleLineString(e, true)); 580 throw new ConfigException(message); 581 } 582 583 try { 584 loadDefinitionClasses(is); 585 } catch (final ConfigException e) { 586 debugLogger.trace("Unable to load classes from input stream", e); 587 final LocalizableMessage message = ERR_CLASS_LOADER_CANNOT_LOAD_EXTENSION.get( 588 jarFile.getName(), MANIFEST_RELATIVE_PATH, stackTraceToSingleLineString(e, true)); 589 throw new ConfigException(message); 590 } 591 try { 592 // Log build information of extensions in the error log 593 final String[] information = getBuildInformation(jarFile); 594 final LocalizableMessage message = NOTE_LOG_EXTENSION_INFORMATION.get( 595 jarFile.getName(), information[1], information[2]); 596 LocalizedLogger.getLocalizedLogger(message.resourceName()).info(message); 597 } catch (final Exception e) { 598 // Do not log information for that extension 599 } 600 } 601 } 602 603 /** 604 * Forcefully load configuration definition classes named in a manifest file. 605 * 606 * @param is 607 * The manifest file input stream. 608 * @throws ConfigException 609 * If the definition classes could not be loaded and initialized. 610 */ 611 private void loadDefinitionClasses(final InputStream is) throws ConfigException { 612 // Cannot use ServiceLoader because constructors are private 613 final BufferedReader reader = new BufferedReader(new InputStreamReader(is)); 614 final List<AbstractManagedObjectDefinition<?, ?>> definitions = new LinkedList<>(); 615 while (true) { 616 String className; 617 try { 618 className = reader.readLine(); 619 } catch (final IOException e) { 620 final LocalizableMessage msg = 621 ERR_CLASS_LOADER_CANNOT_READ_MANIFEST_FILE.get(e.getMessage()); 622 throw new ConfigException(msg, e); 623 } 624 625 // Break out when the end of the manifest is reached. 626 if (className == null) { 627 break; 628 } 629 630 className = className.trim(); 631 // Skip blank lines or lines beginning with #. 632 if (className.isEmpty() || className.startsWith("#")) { 633 continue; 634 } 635 636 debugLogger.trace("Loading class " + className); 637 638 // Load the class and get an instance of it if it is a definition. 639 Class<?> theClass; 640 try { 641 theClass = Class.forName(className, true, loader); 642 } catch (final Exception e) { 643 final LocalizableMessage msg = 644 ERR_CLASS_LOADER_CANNOT_LOAD_CLASS.get(className, e.getMessage()); 645 throw new ConfigException(msg, e); 646 } 647 if (AbstractManagedObjectDefinition.class.isAssignableFrom(theClass)) { 648 // We need to instantiate it using its getInstance() static method. 649 Method method; 650 try { 651 method = theClass.getMethod("getInstance"); 652 } catch (final Exception e) { 653 final LocalizableMessage msg = 654 ERR_CLASS_LOADER_CANNOT_FIND_GET_INSTANCE_METHOD.get(className, e.getMessage()); 655 throw new ConfigException(msg, e); 656 } 657 658 // Get the definition instance. 659 AbstractManagedObjectDefinition<?, ?> d; 660 try { 661 d = (AbstractManagedObjectDefinition<?, ?>) method.invoke(null); 662 } catch (final Exception e) { 663 final LocalizableMessage msg = 664 ERR_CLASS_LOADER_CANNOT_INVOKE_GET_INSTANCE_METHOD.get(className, e.getMessage()); 665 throw new ConfigException(msg, e); 666 } 667 definitions.add(d); 668 } 669 } 670 671 // Initialize any definitions that were loaded. 672 for (final AbstractManagedObjectDefinition<?, ?> d : definitions) { 673 try { 674 d.initialize(); 675 } catch (final Exception e) { 676 final LocalizableMessage msg = ERR_CLASS_LOADER_CANNOT_INITIALIZE_DEFN.get( 677 d.getName(), d.getClass().getName(), e.getMessage()); 678 throw new ConfigException(msg, e); 679 } 680 } 681 } 682 683 private JarFile loadJarFile(final File jar) throws ConfigException { 684 try { 685 // Load the extension jar file. 686 return new JarFile(jar); 687 } catch (final Exception e) { 688 debugLogger.trace("Unable to load jar file: " + jar, e); 689 690 final LocalizableMessage message = 691 ERR_ADMIN_CANNOT_OPEN_JAR_FILE.get(jar.getName(), jar.getParent(), 692 stackTraceToSingleLineString(e, true)); 693 throw new ConfigException(message); 694 } 695 } 696 697 /** 698 * Returns the installation path. 699 * 700 * @return The installation path of this instance. 701 */ 702 public String getInstallPath() { 703 return installPath; 704 } 705 706 /** 707 * Returns the instance path. 708 * 709 * @return The instance path. 710 */ 711 public String getInstancePath() { 712 return instancePath; 713 } 714}