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