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