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}