diff --git a/exist-core/src/main/java/org/exist/repo/ClasspathHelper.java b/exist-core/src/main/java/org/exist/repo/ClasspathHelper.java index 8aa2a99a0fd..1a1d016afe6 100644 --- a/exist-core/src/main/java/org/exist/repo/ClasspathHelper.java +++ b/exist-core/src/main/java/org/exist/repo/ClasspathHelper.java @@ -24,8 +24,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.SystemProperties; -import org.exist.start.Classpath; -import org.exist.start.EXistClassLoader; +import org.exist.start.classloader.Classpath; +import org.exist.start.classloader.EXistClassLoader; import org.exist.storage.BrokerPool; import org.exist.storage.BrokerPoolService; import org.expath.pkg.repo.FileSystemStorage; diff --git a/exist-core/src/main/java/org/exist/test/ExistEmbeddedServer.java b/exist-core/src/main/java/org/exist/test/ExistEmbeddedServer.java index 108a6d4be6b..b5ea7f38033 100644 --- a/exist-core/src/main/java/org/exist/test/ExistEmbeddedServer.java +++ b/exist-core/src/main/java/org/exist/test/ExistEmbeddedServer.java @@ -24,8 +24,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.EXistException; -import org.exist.start.Classpath; -import org.exist.start.EXistClassLoader; +import org.exist.start.classloader.Classpath; +import org.exist.start.classloader.EXistClassLoader; import org.exist.storage.BrokerPool; import org.exist.storage.journal.Journal; import org.exist.util.Configuration; diff --git a/exist-core/src/main/java/org/exist/webstart/JnlpJarFiles.java b/exist-core/src/main/java/org/exist/webstart/JnlpJarFiles.java index 71fb193204a..585dccc95fa 100644 --- a/exist-core/src/main/java/org/exist/webstart/JnlpJarFiles.java +++ b/exist-core/src/main/java/org/exist/webstart/JnlpJarFiles.java @@ -33,7 +33,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.exist.start.LatestFileResolver; import org.exist.util.FileUtils; /** diff --git a/exist-start/src/main/java/org/exist/start/LatestFileResolver.java b/exist-core/src/main/java/org/exist/webstart/LatestFileResolver.java similarity index 70% rename from exist-start/src/main/java/org/exist/start/LatestFileResolver.java rename to exist-core/src/main/java/org/exist/webstart/LatestFileResolver.java index f3e9c36bde7..f5708f10c28 100644 --- a/exist-start/src/main/java/org/exist/start/LatestFileResolver.java +++ b/exist-core/src/main/java/org/exist/webstart/LatestFileResolver.java @@ -19,25 +19,28 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.exist.start; +package org.exist.webstart; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import java.util.List; +import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * This class uses regex pattern matching to find the latest version of a - * particular jar file. - * - * @see LatestFileResolver#getResolvedFileName(String) - * + * particular jar file. + * * @author Ben Schmaus (exist@benschmaus.com) * @version $Revision$ + * @see LatestFileResolver#getResolvedFileName(String) */ public class LatestFileResolver { @@ -45,21 +48,19 @@ public class LatestFileResolver { // latest version of a particular file should be added to the classpath. // E.g., commons-fileupload-%latest%.jar would resolve to something like // commons-fileupload-1.1.jar. - private final static Pattern latestVersionPattern = Pattern.compile( - "(%latest%)" - ); + private final static Pattern latestVersionPattern = Pattern.compile("(%latest%)"); // Set debug mode for each file resolver instance based on whether or // not the system was started with debugging turned on. - private static boolean _debug = Boolean.getBoolean("exist.start.debug"); - + private static final boolean _debug = Boolean.getBoolean("exist.start.debug"); + /** * If the passed file name contains a %latest% token, * find the latest version of that file. Otherwise, return * the passed file name unmodified. - * + * * @param filename Path relative to exist home dir of - * a jar file that should be added to the classpath. + * a jar file that should be added to the classpath. * @return Resolved filename. */ public String getResolvedFileName(final String filename) { @@ -72,9 +73,7 @@ public String getResolvedFileName(final String filename) { final String uptoToken = fileinfo[0]; // Dir that should contain our jar. - final String containerDirName = uptoToken.substring( - 0, uptoToken.lastIndexOf(File.separatorChar) - ); + final String containerDirName = uptoToken.substring(0, uptoToken.lastIndexOf(File.separatorChar)); final Path containerDir = Paths.get(containerDirName); @@ -86,8 +85,8 @@ public String getResolvedFileName(final String filename) { List jars; try { - jars = Main.list(containerDir, p -> { - matcher.reset(Main.fileName(p)); + jars = list(containerDir, p -> { + matcher.reset(fileName(p)); return matcher.find(); }); } catch (final IOException e) { @@ -99,20 +98,34 @@ public String getResolvedFileName(final String filename) { if (!jars.isEmpty()) { final String actualFileName = jars.getFirst().toAbsolutePath().toString(); if (_debug) { - System.err.println( - "Found match: " + actualFileName - + " for jar file pattern: " + filename - ); + System.err.println("Found match: " + actualFileName + " for jar file pattern: " + filename); } return actualFileName; } else { if (_debug) { - System.err.println( - "WARN: No latest version found for JAR file: '" - + filename + "'" - ); + System.err.println("WARN: No latest version found for JAR file: '" + filename + "'"); } } return filename; - } + } + + /** + * Copied from {@link org.exist.util.FileUtils#list(Path, Predicate)} + * as org.exist.start is compiled into a separate Jar and doesn't have + * the rest of eXist available on the classpath + */ + static List list(final Path directory, final Predicate filter) throws IOException { + try (final Stream entries = Files.list(directory).filter(filter)) { + return entries.collect(Collectors.toList()); + } + } + + /** + * Copied from {@link org.exist.util.FileUtils#fileName(Path)} + * as org.exist.start is compiled into a separate Jar and doesn't have + * the rest of eXist available on the classpath + */ + static String fileName(final Path path) { + return path.getFileName().toString(); + } } diff --git a/exist-distribution/pom.xml b/exist-distribution/pom.xml index 078e49d0665..09da309233f 100644 --- a/exist-distribution/pom.xml +++ b/exist-distribution/pom.xml @@ -1057,7 +1057,7 @@ org.exist.start.Main org.exist.start.Main eXist-JavaAppLauncher - 1.8 + 21 10.9 ${project.version} ${project.version} diff --git a/exist-service/src/main/java/org/exist/service/ExistDbDaemon.java b/exist-service/src/main/java/org/exist/service/ExistDbDaemon.java index d9e2a2300ba..48ae21efbcf 100644 --- a/exist-service/src/main/java/org/exist/service/ExistDbDaemon.java +++ b/exist-service/src/main/java/org/exist/service/ExistDbDaemon.java @@ -36,7 +36,7 @@ public class ExistDbDaemon implements Daemon { private String[] args = null; private void init(final String args[]) { - this.main = new Main("jetty"); + this.main = new Main(Main.MODE_JETTY); this.args = args; } @@ -65,12 +65,12 @@ public void start() throws Exception { runArgs[0] = MODE_JETTY; System.arraycopy(args, 0, runArgs, 1, args.length); - this.main.runEx(runArgs); + this.main.startExistdb(runArgs); } @Override public void stop() throws Exception { - this.main.shutdownEx(); + this.main.shutdownExistdb(); } @Override diff --git a/exist-start/pom.xml b/exist-start/pom.xml index f07d113ebff..cee8aa18b4f 100644 --- a/exist-start/pom.xml +++ b/exist-start/pom.xml @@ -76,9 +76,7 @@ --> Mortbay-APACHE-2-license.template.txt - src/main/java/org/exist/start/Classpath.java - src/main/java/org/exist/start/Main.java - src/main/java/org/exist/start/Version.java + src/main/java/org/exist/start/classloader/Classpath.java @@ -95,9 +93,7 @@ The original license statement is also included below.]]> - src/main/java/org/exist/start/Classpath.java - src/main/java/org/exist/start/Main.java - src/main/java/org/exist/start/Version.java + src/main/java/org/exist/start/classloader/Classpath.java diff --git a/exist-start/src/main/java/org/exist/start/CompatibleJavaVersionCheck.java b/exist-start/src/main/java/org/exist/start/CompatibleJavaVersionCheck.java index ed76113e83d..09e2ba339c3 100644 --- a/exist-start/src/main/java/org/exist/start/CompatibleJavaVersionCheck.java +++ b/exist-start/src/main/java/org/exist/start/CompatibleJavaVersionCheck.java @@ -24,37 +24,49 @@ import java.util.Optional; import java.util.stream.Stream; -import static org.exist.start.CompatibleJavaVersionCheck.IncompatibleJavaVersion.IncompatibleJavaVersion; +import static org.exist.start.CompatibleJavaVersionCheck.IncompatibleJavaVersion.create; import static org.exist.start.Main.ERROR_CODE_INCOMPATIBLE_JAVA_DETECTED; +/** + * Helper class for checking OpenJDK compatibility. + *

+ * eXist-db has been compiled with Java21 (state of 2025Q2). + *

+ * Older versions of Java contained a number of serious compiler bugs that caused database corruptions. + * These problematic versions are deprecated and therefore this class is not relevant anymore. + *

+ * The code is kept for archival purposes and potential future re-usage. + *

+ * ---------------------------- + *

+ * OpenJDK versions 12 through 15.0.1 suffer from a critical bug in the JIT C2 compiler that will + * cause data loss in eXist-db. The problem has been reported to the OpenJDK community. + *

+ * For more information, see: + * - C2: Masked byte comparisons with large masks produce wrong result on x86 + * - eXist-db does not run correctly on JDKs 12, 13, 14 and 15 #3375 + * + * + */ public class CompatibleJavaVersionCheck { private static final IncompatibleJavaVersion[] INCOMPATIBLE_JAVA_VERSIONS = { - IncompatibleJavaVersion(12), - IncompatibleJavaVersion(13), - IncompatibleJavaVersion(14), - IncompatibleJavaVersion(15, 0, 2) + create(12), + create(13), + create(14), + IncompatibleJavaVersion.create(15, 0, 2), }; private static final String INCOMPATIBLE_JAVA_VERSION_NOTICE = - "*****************************************************%n" + - "Warning: Unreliable Java version has been detected!%n" + - "%n" + - "OpenJDK versions 12 through 15.0.1 suffer from a critical%n" + - " bug in the JIT C2 compiler that will cause data loss in%n" + - "eXist-db.%n" + - "%n" + - "The problem has been reported to the OpenJDK community.%n" + - "%n" + - "For more information, see:%n" + - "\t* https://bugs.openjdk.java.net/browse/JDK-8253191%n" + - "\t* https://github.com/eXist-db/exist/issues/3375%n" + - "%n" + - "The detected version of Java on your system is: %s.%n" + - "%n" + - "To prevent potential data loss, eXist-db will not be started.%n" + - "To start eXist-db, we recommend using Java 8 or 11.%n" + - "*****************************************************"; + "*****************************************************%n" + + "Incorrect version of Java detected!%n" + + "%n" + + "The detected version of Java on your system is: %s.%n" + + "%n" + + "eXist-db has been developed and qualified using Java 21.%n" + + "%n" + + "Newer versions of Java might or might not work correctly.%n" + + "*****************************************************"; private static final Optional RUNTIME_JAVA_VERSION = Optional.ofNullable(System.getProperty("java.version")); @@ -71,7 +83,7 @@ public static void checkForCompatibleJavaVersion() throws StartException { static void checkForCompatibleJavaVersion(final Optional checkJavaVersion) throws StartException { final Optional maybeJavaVersionComponents = extractJavaVersionComponents(checkJavaVersion); - if (!maybeJavaVersionComponents.isPresent()) { + if (maybeJavaVersionComponents.isEmpty()) { // Could not determine major java version, so best to let the user proceed... return; } @@ -79,8 +91,10 @@ static void checkForCompatibleJavaVersion(final Optional checkJavaVersio // check for incompatible java version final int[] javaVersionComponents = maybeJavaVersionComponents.get(); final int majorJavaVersion = javaVersionComponents[0]; - /* @Nullable */ final Integer minorJavaVersion = javaVersionComponents.length > 1 ? javaVersionComponents[1] : null; - /* @Nullable */ final Integer patchJavaVersion = javaVersionComponents.length > 2 ? javaVersionComponents[2] : null; + /* @Nullable */ + final Integer minorJavaVersion = javaVersionComponents.length > 1 ? javaVersionComponents[1] : null; + /* @Nullable */ + final Integer patchJavaVersion = javaVersionComponents.length > 2 ? javaVersionComponents[2] : null; for (final IncompatibleJavaVersion incompatibleJavaVersion : INCOMPATIBLE_JAVA_VERSIONS) { // compare major versions @@ -104,7 +118,8 @@ static void checkForCompatibleJavaVersion(final Optional checkJavaVersio } // version is NOT compatible! - throw new StartException(ERROR_CODE_INCOMPATIBLE_JAVA_DETECTED, String.format(INCOMPATIBLE_JAVA_VERSION_NOTICE, RUNTIME_JAVA_VERSION)); + throw new StartException(ERROR_CODE_INCOMPATIBLE_JAVA_DETECTED, + String.format(INCOMPATIBLE_JAVA_VERSION_NOTICE, RUNTIME_JAVA_VERSION.orElse("UKNOWN"))); } // version is compatible @@ -131,22 +146,22 @@ static class IncompatibleJavaVersion { /* @Nullable */ final Integer lessThanMinor; /* @Nullable */ final Integer lessThanPatch; - private IncompatibleJavaVersion(final int major, /* @Nullable */ Integer lessThanMinor, /* @Nullable */ Integer lessThanPatch) { + private IncompatibleJavaVersion(final int major, /* @Nullable */ final Integer lessThanMinor, /* @Nullable */ final Integer lessThanPatch) { this.major = major; this.lessThanMinor = lessThanMinor; this.lessThanPatch = lessThanPatch; } - public static IncompatibleJavaVersion IncompatibleJavaVersion(final int major, /* @Nullable */ Integer lessThanMinor, /* @Nullable */ Integer lessThanPatch) { + public static IncompatibleJavaVersion create(final int major, /* @Nullable */ final Integer lessThanMinor, /* @Nullable */ final Integer lessThanPatch) { return new IncompatibleJavaVersion(major, lessThanMinor, lessThanPatch); } - public static IncompatibleJavaVersion IncompatibleJavaVersion(final int major, /* @Nullable */ Integer lessThanMinor) { - return IncompatibleJavaVersion(major, lessThanMinor, null); + public static IncompatibleJavaVersion create(final int major, /* @Nullable */ final Integer lessThanMinor) { + return new IncompatibleJavaVersion(major, lessThanMinor, null); } - public static IncompatibleJavaVersion IncompatibleJavaVersion(final int major) { - return IncompatibleJavaVersion(major, null, null); + public static IncompatibleJavaVersion create(final int major) { + return new IncompatibleJavaVersion(major, null, null); } } } diff --git a/exist-start/src/main/java/org/exist/start/Main.java b/exist-start/src/main/java/org/exist/start/Main.java index 59475223b47..0d49d6efdd3 100644 --- a/exist-start/src/main/java/org/exist/start/Main.java +++ b/exist-start/src/main/java/org/exist/start/Main.java @@ -1,7 +1,4 @@ /* - * NOTE: This file is in part based on code from Mort Bay Consulting. - * The original license statement is also included below. - * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -21,75 +18,66 @@ * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * --------------------------------------------------------------------- - * - * Copyright 2002-2005 Mort Bay Consulting Pty. Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package org.exist.start; -import java.io.*; +import org.exist.start.classloader.EXistClassLoader; +import org.exist.start.classloader.ReflectionUtils; + import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.*; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.Optional; /** - * This is an adopted version of the corresponding classes shipped - * with Jetty. Modified for eXist-db! - * - * @author Jan Hlavaty (hlavac@code.cz) - * @author Wolfgang Meier (meier@ifs.tu-darmstadt.de) - * @version $Revision$ - * TODO: - * - finish possible jetty.home locations - * - better handling of errors (i.e. when jetty.home cannot be autodetected...) - * - include entries from lib _when needed_ + * eXist-db bootstrap start helper. + *

+ * The purpose of the class is to setup and configure the (java) environment + * before the database is actually started, by invoking other classes. + *

+ * The class is designed to have only dependencies with java.* packages. + *

+ * The original class was based on Jetty startup code (Mort Bay Consulting, Apache licenced) + * but has completely revised and reimplemented over time. */ public class Main { - public static final String STANDARD_ENABLED_JETTY_CONFIGS = "standard.enabled-jetty-configs"; - public static final String STANDALONE_ENABLED_JETTY_CONFIGS = "standalone.enabled-jetty-configs"; + + public static final String CONFIG_DIR_NAME = "etc"; + public static final String ENV_EXIST_HOME = "EXIST_HOME"; + public static final String ENV_EXIST_JETTY_CONFIG = "EXIST_JETTY_CONFIG"; + public static final String ENV_JETTY_HOME = "JETTY_HOME"; + public static final String PROP_EXIST_HOME = "exist.home"; + public static final String PROP_EXIST_JETTY_CONFIG = "exist.jetty.config"; + public static final String PROP_JETTY_HOME = "jetty.home"; public static final String PROP_LOG4J_DISABLEJMX = "log4j2.disableJmx"; public static final String PROP_XML_CATALOG_ALWAYS_RESOLVE = "xml.catalog.alwaysResolve"; - + public static final String STANDALONE_ENABLED_JETTY_CONFIGS = "standalone.enabled-jetty-configs"; + public static final String STANDARD_ENABLED_JETTY_CONFIGS = "standard.enabled-jetty-configs"; + public static final String MODE_JETTY = "jetty"; + public static final String MODE_STANDALONE = "standalone"; + public static final String MODE_OTHER = "other"; + public static final String MODE_CLIENT = "client"; + public static final String MODE_BACKUP = "backup"; + static final int ERROR_CODE_INCOMPATIBLE_JAVA_DETECTED = 13; private static final int ERROR_CODE_GENERAL = 1; private static final int ERROR_CODE_NO_JETTY_CONFIG = 7; - static final int ERROR_CODE_INCOMPATIBLE_JAVA_DETECTED = 13; - - public static final String CONFIG_DIR_NAME = "etc"; - private static final String PROP_EXIST_START_DEBUG = "exist.start.debug"; - public static final String PROP_EXIST_JETTY_CONFIG = "exist.jetty.config"; - public static final String PROP_EXIST_HOME = "exist.home"; - public static final String PROP_JETTY_HOME = "jetty.home"; - private static final String PROP_LOG4J_CONFIGURATION_FILE = "log4j.configurationFile"; - private static final String PROP_JUL_MANAGER = "java.util.logging.manager"; private static final String PROP_JAVA_TEMP_DIR = "java.io.tmpdir"; - - public static final String ENV_EXIST_JETTY_CONFIG = "EXIST_JETTY_CONFIG"; - public static final String ENV_EXIST_HOME = "EXIST_HOME"; - public static final String ENV_JETTY_HOME = "JETTY_HOME"; - + private static final String PROP_JUL_MANAGER = "java.util.logging.manager"; + private static final String PROP_LOG4J_CONFIGURATION_FILE = "log4j.configurationFile"; private static Main exist; + private final boolean inDebugMode = Boolean.getBoolean(PROP_EXIST_START_DEBUG); + private String mode = MODE_OTHER; + + private Main() { + } - private String _mode = "jetty"; - private boolean _debug = Boolean.getBoolean(PROP_EXIST_START_DEBUG); + public Main(final String mode) { + this.mode = mode; + } public static void main(final String[] args) { try { @@ -113,50 +101,60 @@ public static Main getMain() { return exist; } - public String getMode() { - return this._mode; - } + /** + * Configures the Log4j2 logging framework by establishing the appropriate logging configuration file + * and enabling necessary properties to ensure proper logging behavior. + *

+ * This method searches for a Log4j2 configuration file in multiple locations: + * 1. Directly through a system property. + * 2. In a specified home directory, if available. + *

+ * It also configures additional system properties, such as setting up a JUL bridge for Log4j2 + * and enabling JMX support. + * + * @param existHomeDir an optional path to the eXist-db home directory where the Log4j2 configuration + * file may be located + */ + private static void setupLog4j2(final Optional existHomeDir) { - private Main() { - } + // Get path from system property + Optional log4jConfigurationFile = Optional.ofNullable(System.getProperty(PROP_LOG4J_CONFIGURATION_FILE)).map(Paths::get); - public Main(final String mode) { - this._mode = mode; - } + // Try to find configuration file is not already found. + if (log4jConfigurationFile.isEmpty()) { - private static Path getDirectory(final String name) { - try { - if (name != null) { - final Path dir = Paths.get(name).normalize().toAbsolutePath(); - if (Files.isDirectory(dir)) { - return dir; - } + // Try to find file in eXist-db directory. + if (existHomeDir.isPresent() && Files.exists(existHomeDir.get().resolve(CONFIG_DIR_NAME))) { + log4jConfigurationFile = existHomeDir.map(f -> f.resolve(CONFIG_DIR_NAME).resolve("log4j2.xml")); } - } catch (final InvalidPathException e) { - // NOP - } - return null; - } - private static void invokeMain(final ClassLoader classloader, final String classname, final String[] args) - throws IllegalAccessException, InvocationTargetException, - NoSuchMethodException, ClassNotFoundException { + // If file was found, update system property. + if (log4jConfigurationFile.isPresent() && Files.isReadable(log4jConfigurationFile.get())) { + System.setProperty(PROP_LOG4J_CONFIGURATION_FILE, log4jConfigurationFile.get().toAbsolutePath().toString()); + } + } - final Class invoked_class = classloader.loadClass(classname); + if (log4jConfigurationFile.isPresent()) { + // redirect JUL to log4j2 unless otherwise specified + System.setProperty(PROP_JUL_MANAGER, + Optional.ofNullable(System.getProperty(PROP_JUL_MANAGER)).orElse("org.apache.logging.log4j.jul.LogManager")); + } - final Class[] method_param_types = new Class[1]; - method_param_types[0] = args.getClass(); + // Enable JMX support log4j since v2.24.0 [2024] + System.setProperty(PROP_LOG4J_DISABLEJMX, "false"); + } - final Method main = invoked_class.getDeclaredMethod("main", method_param_types); + private String getMode() { + return mode; + } - final Object[] method_params = new Object[1]; - method_params[0] = args; - main.invoke(null, method_params); + private void setMode(final String mode) { + this.mode = mode; } public void run(final String[] args) { try { - runEx(args); + startExistdb(args); } catch (final StartException e) { if (e.getMessage() != null && !e.getMessage().isEmpty()) { System.err.println(e.getMessage()); @@ -165,121 +163,160 @@ public void run(final String[] args) { } } - - - public void runEx(String[] args) throws StartException { + /** + * Starts the eXist-db application by initializing required configurations, verifying + * Java compatibility, setting up logging, and configuring the appropriate runtime environment + * based on specified modes such as Jetty or standalone. + * This method resolves relevant arguments and invokes the main class using a custom + * class loader for bootstrapping the application. + * + * @param args an array of strings representing the command-line arguments; the first + * argument determines the mode or main class to be invoked, followed by + * additional configuration-specific arguments. It may be modified internally + * during the execution to adjust for specific configurations. + * @throws StartException if any error occurs during the startup process such as + * incompatible Java version, misconfiguration, or failure to + * resolve required resources. + */ + public void startExistdb(String[] args) throws StartException { // Check if the OpenJDK version can corrupt eXist-db CompatibleJavaVersionCheck.checkForCompatibleJavaVersion(); - final String _classname; - if (args.length > 0) { - switch (args[0]) { - case "client" -> { - _classname = "org.exist.client.InteractiveClient"; - _mode = "client"; - } - case "backup" -> { - _classname = "org.exist.backup.Main"; - _mode = "backup"; - } - case "jetty", "standalone" -> { - _classname = "org.exist.jetty.JettyStart"; - _mode = args[0]; - } - case "launch" -> { - _classname = "org.exist.launcher.LauncherWrapper"; - _mode = "jetty"; - } - case "launcher" -> { - _classname = "org.exist.launcher.LauncherWrapper"; - _mode = "other"; - } - case "shutdown" -> { - _classname = "org.exist.jetty.ServerShutdown"; - _mode = "other"; - } - case null, default -> { - _classname = args[0]; - _mode = "other"; - } - } - - final String[] nargs = new String[args.length - 1]; - if (args.length > 1) { - System.arraycopy(args, 1, nargs, 0, args.length - 1); - } - args = nargs; - - } else { - _classname = "org.exist.launcher.LauncherWrapper"; - _mode = "other"; - } + final String mainClassName = getClassName(args); - if (_debug) { - System.err.println("mode=" + _mode); - } + args = stripFirstElement(args); // try and figure out exist home dir final Optional existHomeDir = getFromSysPropOrEnv(PROP_EXIST_HOME, ENV_EXIST_HOME).map(Paths::get); // try to find Jetty - if ("jetty".equals(_mode) || "standalone".equals(_mode)) { - final Optional jettyHomeDir = getFromSysPropOrEnv(PROP_JETTY_HOME, ENV_JETTY_HOME).map(Paths::get); + if (MODE_JETTY.equals(getMode()) || MODE_STANDALONE.equals(getMode())) { + final Path jettyConfig = configureForJetty(existHomeDir); + args = addFirstElement(args, jettyConfig); + } - Optional existJettyConfigFile = getFromSysPropOrEnv(PROP_EXIST_JETTY_CONFIG, ENV_EXIST_JETTY_CONFIG).map(Paths::get); - if (!existJettyConfigFile.isPresent()) { - final String config; - if ("jetty".equals(_mode)) { - config = STANDARD_ENABLED_JETTY_CONFIGS; - } else { - config = STANDALONE_ENABLED_JETTY_CONFIGS; - } + setupLog4j2(existHomeDir); - if (jettyHomeDir.isPresent() && Files.exists(jettyHomeDir.get().resolve(CONFIG_DIR_NAME))) { - existJettyConfigFile = jettyHomeDir.map(f -> f.resolve(CONFIG_DIR_NAME).resolve(config)); - } + // Modify behavior XML resolver for > 5.x [2024] + System.setProperty(PROP_XML_CATALOG_ALWAYS_RESOLVE, "false"); - if (existHomeDir.isPresent() && Files.exists(existHomeDir.get().resolve(CONFIG_DIR_NAME))) { - existJettyConfigFile = existHomeDir.map(f -> f.resolve(CONFIG_DIR_NAME).resolve(config)); - } + // Clean up tempdir for Jetty... + tweakTempDirectory(); - if (!existJettyConfigFile.isPresent()) { - System.err.println("ERROR: jetty config file could not be found! Make sure to set exist.jetty.config or EXIST_JETTY_CONFIG."); - System.err.flush(); - throw new StartException(ERROR_CODE_NO_JETTY_CONFIG); - } - } - final String[] jettyStartArgs = new String[1 + args.length]; - jettyStartArgs[0] = existJettyConfigFile.get().toAbsolutePath().toString(); - System.arraycopy(args, 0, jettyStartArgs, 1, args.length); - args = jettyStartArgs; + // Setup classloader + final EXistClassLoader eXistClassLoader = ReflectionUtils.getEXistClassLoader(); + + // Invoke main class using new classloader. + try { + ReflectionUtils.invokeMain(eXistClassLoader, mainClassName, args); + } catch (final Exception e) { + e.printStackTrace(); + throw new StartException(ERROR_CODE_GENERAL); } + } - // find log4j2.xml - Optional log4jConfigurationFile = Optional.ofNullable(System.getProperty(PROP_LOG4J_CONFIGURATION_FILE)).map(Paths::get); - if (!log4jConfigurationFile.isPresent()) { - if (existHomeDir.isPresent() && Files.exists(existHomeDir.get().resolve(CONFIG_DIR_NAME))) { - log4jConfigurationFile = existHomeDir.map(f -> f.resolve(CONFIG_DIR_NAME).resolve("log4j2.xml")); - } + /** + * Adds a specified element, represented by the absolute path of the provided `jettyConfig`, + * as the first element in a new array and appends all elements of the input array to it. + * This method creates a new array combining the provided `jettyConfig` as the first argument + * followed by the elements of the given `args` array. + * + * @param args an array of strings representing the original arguments; must not be null + * @param jettyConfig the `Path` object whose absolute path is added as the first element + * in the new array; must not be null + * @return a new array of strings where the first element is the absolute path of the + * given `jettyConfig` and all subsequent elements are from the provided `args` array + */ + private static String[] addFirstElement(final String[] args, final Path jettyConfig) { + final String[] jettyStartArgs = new String[1 + args.length]; + jettyStartArgs[0] = jettyConfig.toAbsolutePath().toString(); + System.arraycopy(args, 0, jettyStartArgs, 1, args.length); + return jettyStartArgs; + } - if (log4jConfigurationFile.isPresent() && Files.isReadable(log4jConfigurationFile.get())) { - System.setProperty(PROP_LOG4J_CONFIGURATION_FILE, log4jConfigurationFile.get().toAbsolutePath().toString()); - } + /** + * Removes the first element from the provided array of strings and returns the resulting array. + * If the input array has one or no elements, an empty array is returned. + * + * @param args an array of strings from which the first element is to be removed; + * must not be null, but can be empty + * @return a new array containing all elements of the input array except the first one; + * if the input array contains no elements or only one element, an empty array is returned + */ + private static String[] stripFirstElement(final String[] args) { + final String[] newArguments = new String[args.length - 1]; + if (args.length > 1) { + System.arraycopy(args, 1, newArguments, 0, args.length - 1); } + return newArguments; + } - if (log4jConfigurationFile.isPresent()) { - //redirect JUL to log4j2 unless otherwise specified - System.setProperty(PROP_JUL_MANAGER, Optional.ofNullable(System.getProperty(PROP_JUL_MANAGER)).orElse("org.apache.logging.log4j.jul.LogManager")); + /** + * Determines and returns the class name to be used based on the provided arguments. + * The method evaluates the first argument in the provided array and maps it to a + * corresponding class name. If no arguments are provided or the arguments are invalid, + * a default class name is returned. + * + * @param args an array of strings representing command-line arguments; can be null or empty + * @return the fully qualified name of the class as a string + */ + private String getClassName(final String[] args) { + + final String className; + + if (args == null || args.length == 0) { + className = "org.exist.launcher.LauncherWrapper"; + setMode(MODE_OTHER); + + } else { + final String firstArgument = args[0]; + switch (firstArgument) { + case MODE_CLIENT -> { + className = "org.exist.client.InteractiveClient"; + setMode(firstArgument); + } + case MODE_BACKUP -> { + className = "org.exist.backup.Main"; + setMode(firstArgument); + } + case MODE_JETTY, MODE_STANDALONE -> { + className = "org.exist.jetty.JettyStart"; + setMode(firstArgument); + } + case "launch" -> { + className = "org.exist.launcher.LauncherWrapper"; + setMode(MODE_JETTY); + } + case "launcher" -> className = "org.exist.launcher.LauncherWrapper"; + case "shutdown" -> className = "org.exist.jetty.ServerShutdown"; + case null, default -> className = firstArgument; + } } - // Enable JXM support log4j since v2.24.0 [2024] - System.setProperty(PROP_LOG4J_DISABLEJMX, "false"); + if (inDebugMode) { + System.err.println("mode=" + getMode()); + } - // Modify behavior XML resolver for > 5.x [2024] - System.setProperty(PROP_XML_CATALOG_ALWAYS_RESOLVE,"false"); + return className; + } - // clean up tempdir for Jetty... + /** + * Adjusts the value of the system property for the temporary directory used by the Java application. + * This method ensures the value of the temporary directory system property is set to an absolute + * path if the directory exists, and logs the value if the application is in debug mode. + *

+ * Behavior: + * - Retrieves the system property for the Java temporary directory. + * - Converts the property value into an absolute path and verifies if it points to an existing directory. + * - If the directory exists, the property is updated with the absolute path. + * - Logs the value of the temporary directory system property to the error stream if debugging is enabled. + *

+ * Exceptions: + * - Catches and silently ignores {@link InvalidPathException} if the system property contains + * an invalid path or cannot be resolved as a valid directory. + */ + private void tweakTempDirectory() { try { final Path tmpdir = Paths.get(System.getProperty(PROP_JAVA_TEMP_DIR)).toAbsolutePath(); if (Files.isDirectory(tmpdir)) { @@ -290,77 +327,118 @@ public void runEx(String[] args) throws StartException { // ignore } - if (_debug) { + if (inDebugMode) { System.err.println(PROP_JAVA_TEMP_DIR + "=" + System.getProperty(PROP_JAVA_TEMP_DIR)); } + } - // setup classloader - final Classpath _classpath = new Classpath(); - final EXistClassLoader cl = _classpath.getClassLoader(null); - Thread.currentThread().setContextClassLoader(cl); + /** + * Configures the system to use the appropriate Jetty configuration file based on the provided + * eXist-db home directory and other system parameters. The method attempts to resolve the + * configuration file either from system properties, environment variables, or defaults to a + * pre-configured standard or standalone setup. If the configuration cannot be located, a + * {@code StartException} is thrown. + * + * @param existHomeDir an optional path to the eXist-db home directory where the configuration + * may be located + * @return the path to the resolved Jetty configuration file + * @throws StartException if no valid configuration file can be located + */ + private Path configureForJetty(final Optional existHomeDir) throws StartException { - // Invoke main class using new classloader. - try { - invokeMain(cl, _classname, args); - } catch (final Exception e) { - e.printStackTrace(); - throw new StartException(ERROR_CODE_GENERAL); + // Get configured path for Jetty config file + Optional existJettyConfigFile = getFromSysPropOrEnv(PROP_EXIST_JETTY_CONFIG, ENV_EXIST_JETTY_CONFIG).map(Paths::get); + + // If configuration was not found + if (existJettyConfigFile.isEmpty()) { + + // Detect 'Normal Jetty" or "Standalone" modus. + final String config = MODE_JETTY.equals(getMode()) + ? STANDARD_ENABLED_JETTY_CONFIGS + : STANDALONE_ENABLED_JETTY_CONFIGS; + + // Get path for jetty homedir + final Optional jettyHomeDir = getFromSysPropOrEnv(PROP_JETTY_HOME, ENV_JETTY_HOME).map(Paths::get); + + // Load configuration from Jetty directory + if (jettyHomeDir.isPresent() && Files.exists(jettyHomeDir.get().resolve(CONFIG_DIR_NAME))) { + existJettyConfigFile = jettyHomeDir.map(f -> f.resolve(CONFIG_DIR_NAME).resolve(config)); + } + + // Load configuration from eXist-db directory + if (existHomeDir.isPresent() && Files.exists(existHomeDir.get().resolve(CONFIG_DIR_NAME))) { + existJettyConfigFile = existHomeDir.map(f -> f.resolve(CONFIG_DIR_NAME).resolve(config)); + } + + // Verify that jetty configuration could be found + if (existJettyConfigFile.isEmpty()) { + System.err.println("ERROR: jetty config file could not be found! Make sure to set exist.jetty.config or EXIST_JETTY_CONFIG."); + System.err.flush(); + throw new StartException(ERROR_CODE_NO_JETTY_CONFIG); + } } + + return existJettyConfigFile.get(); } + /** + * Retrieves a value for a given configuration key either from a system property or an environment variable. + * If the value is found in the environment variable and not in the system property, the method sets the system + * property with the retrieved value. + * + * @param sysPropName the name of the system property to look up + * @param envVarName the name of the environment variable to look up + * @return an {@code Optional} containing the retrieved value if found, + * or an empty {@code Optional} if no value is available + */ private Optional getFromSysPropOrEnv(final String sysPropName, final String envVarName) { Optional value = Optional.ofNullable(System.getProperty(sysPropName)); - if (!value.isPresent()) { + + // Not found in system property, try environment + if (value.isEmpty()) { value = Optional.ofNullable(System.getenv().get(envVarName)); + // if we managed to detect from environment, store it in a system property value.ifPresent(s -> System.setProperty(sysPropName, s)); } - if (_debug && value.isPresent()) { + // Report out + if (inDebugMode && value.isPresent()) { System.err.println(sysPropName + "=" + System.getProperty(sysPropName)); } return value; } - public void shutdown() { - // only used in test suite - try { - shutdownEx(); - } catch (final StopException e) { - e.printStackTrace(); - } - } - - public void shutdownEx() throws StopException { + /** + * Shuts down the eXist-db database by invoking internal methods to stop + * all broker pools associated with the database. + *

+ * This method utilizes reflection to access and call the internal `stopAll` + * method of the `org.exist.storage.BrokerPools` class. It ensures a clean + * shutdown by disabling certain features while stopping the broker pools. + *

+ * Exceptions: + * - Throws {@link StopException} if any error occurs during the reflective + * invocation, such as the target class or method being unavailable, + * access restrictions, or runtime invocation issues. + *

+ * Note: + * This method is primarily intended for use in test environments. + * + * @throws StopException if the shutdown process encounters an error + */ + public void shutdownExistdb() throws StopException { // only used in test suite try { final Class brokerPool = Class.forName("org.exist.storage.BrokerPools"); final Method stopAll = brokerPool.getDeclaredMethod("stopAll", boolean.class); stopAll.setAccessible(true); stopAll.invoke(null, false); - } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new StopException(e.getMessage(), e); - } - } - /** - * Copied from {@link org.exist.util.FileUtils#list(Path, Predicate)} - * as org.exist.start is compiled into a separate Jar and doesn't have - * the rest of eXist available on the classpath - */ - static List list(final Path directory, final Predicate filter) throws IOException { - try(final Stream entries = Files.list(directory).filter(filter)) { - return entries.collect(Collectors.toList()); + } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException | + InvocationTargetException e) { + throw new StopException(e.getMessage(), e); } } - - /** - * Copied from {@link org.exist.util.FileUtils#fileName(Path)} - * as org.exist.start is compiled into a separate Jar and doesn't have - * the rest of eXist available on the classpath - */ - static String fileName(final Path path) { - return path.getFileName().toString(); - } } diff --git a/exist-start/src/main/java/org/exist/start/Version.java b/exist-start/src/main/java/org/exist/start/Version.java deleted file mode 100644 index 3d83922dfda..00000000000 --- a/exist-start/src/main/java/org/exist/start/Version.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * NOTE: This file is in part based on code from Mort Bay Consulting. - * The original license statement is also included below. - * - * eXist-db Open Source Native XML Database - * Copyright (C) 2001 The eXist-db Authors - * - * info@exist-db.org - * http://www.exist-db.org - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * --------------------------------------------------------------------- - * - * Copyright 2002-2005 Mort Bay Consulting Pty. Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.exist.start; - -/** - * Utility class for parsing and comparing version strings. - * JDK 1.1 compatible. - * @author Jan Hlavatý - */ -public class Version { - - private static final int INFERIOR = -1; - private static final int EQUAL = 0; - private static final int SUPERIOR = 1; - - int _version = 0; - int _revision = 0; - int _subrevision = 0; - String _suffix = ""; - - public Version() { - } - - public Version(final String version_string) { - parse(version_string); - } - - /** - * parses version string in the form version[.revision[.subrevision[extension]]] - * into this instance. - * - * @param version_string Text representation of a version. - */ - public void parse(final String version_string) { - _version = 0; - _revision = 0; - _subrevision = 0; - _suffix = ""; - int pos = 0; - int startpos = 0; - final int endpos = version_string.length(); - while ( (pos < endpos) && Character.isDigit(version_string.charAt(pos))) { - pos++; - } - _version = Integer.parseInt(version_string.substring(startpos,pos)); - if ((pos < endpos) && version_string.charAt(pos)=='.') { - startpos = ++pos; - while ( (pos < endpos) && Character.isDigit(version_string.charAt(pos))) { - pos++; - } - _revision = Integer.parseInt(version_string.substring(startpos,pos)); - } - if ((pos < endpos) && version_string.charAt(pos)=='.') { - startpos = ++pos; - while ( (pos < endpos) && Character.isDigit(version_string.charAt(pos))) { - pos++; - } - _subrevision = Integer.parseInt(version_string.substring(startpos,pos)); - } - if (pos < endpos) { - _suffix = version_string.substring(pos); - } - } - - /** - * @return string representation of this version - */ - public String toString() { - return String.valueOf(_version) + '.' + _revision + '.' + _subrevision + _suffix; - } - - // java.lang.Comparable is Java 1.2! Cannot use it - /** - * Compares with other version. Does not take extension into account, - * as there is no reliable way to order them. - * @param other Version object to be compared. - * @return Constants.INFERIOR if this is older version that other, - * Constants.EQUAL if its same version, - * Constants.SUPERIOR if it's newer version than other - */ - public int compare(final Version other) { - if (other == null) {throw new NullPointerException("other version is null");} - if (this._version < other._version) {return INFERIOR;} - if (this._version > other._version) {return SUPERIOR;} - if (this._revision < other._revision) {return INFERIOR;} - if (this._revision > other._revision) {return SUPERIOR;} - if (this._subrevision < other._subrevision) {return INFERIOR;} - if (this._subrevision > other._subrevision) {return SUPERIOR;} - return EQUAL; - } - - /** - * Check whether this version is in range of versions specified - * - * @param high Highest version, inclusive. - * @param low Lowest version, inclusive. - * @return TRUE if Version is between high and low (inclusive), otherwise FALSE. - */ - public boolean isInRange(final Version low, final Version high) { - return (compare(low)>=0 && compare(high)<=0); - } -} diff --git a/exist-start/src/main/java/org/exist/start/Classpath.java b/exist-start/src/main/java/org/exist/start/classloader/Classpath.java similarity index 64% rename from exist-start/src/main/java/org/exist/start/Classpath.java rename to exist-start/src/main/java/org/exist/start/classloader/Classpath.java index e30302fdd94..bd3444f99bc 100644 --- a/exist-start/src/main/java/org/exist/start/Classpath.java +++ b/exist-start/src/main/java/org/exist/start/classloader/Classpath.java @@ -36,7 +36,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.exist.start; +package org.exist.start.classloader; import java.io.File; import java.net.MalformedURLException; @@ -50,51 +50,51 @@ /** * Class to handle CLASSPATH construction + * * @author Jan Hlavaty */ public class Classpath implements Iterable { - final List _elements = new ArrayList<>(); + private final List classPathElements = new ArrayList<>(); - public Classpath() {} + public Classpath() { + } - public Classpath(final String initial) - { + public Classpath(final String initial) { addClasspath(initial); } - + + /** + * Copied from {@link com.evolvedbinary.j8fu.OptionalUtil#or(Optional, Supplier)} + * as org.exist.start is compiled into a separate Jar and doesn't have + * the rest of eXist available on the classpath + */ + private static Optional or(final Optional left, final Supplier> right) { + if (left.isPresent()) { + return left; + } else { + return right.get(); + } + } + public boolean addComponent(final String component) { if (component != null && !component.isEmpty()) { try { - final Path p = Paths.get(component); - if (Files.exists(p)) - { - final Path key = p.toAbsolutePath(); - if (!_elements.contains(key)) - { - _elements.add(key); - return true; - } - } + final Path path = Paths.get(component); + return addComponent(path); } catch (final InvalidPathException e) { e.printStackTrace(); } } return false; } - - public boolean addComponent(final Path component) { - if (component != null) { - try { - if (Files.exists(component)) { - final Path key = component.toAbsolutePath(); - if (!_elements.contains(key)) { - _elements.add(key); - return true; - } - } - } catch (final InvalidPathException e) { - e.printStackTrace(); + + public boolean addComponent(final Path path) throws InvalidPathException { + if (path != null & Files.exists(path)) { + final Path key = path.toAbsolutePath(); + if (!classPathElements.contains(key)) { + classPathElements.add(key); + return true; } } return false; @@ -103,68 +103,58 @@ public boolean addComponent(final Path component) { public void addClasspath(final String s) { if (s != null) { final StringTokenizer t = new StringTokenizer(s, File.pathSeparator); - while (t.hasMoreTokens()) - { + while (t.hasMoreTokens()) { addComponent(t.nextToken()); } } - } + } @Override public String toString() { final StringBuilder cp = new StringBuilder(1024); - final int cnt = _elements.size(); - if (cnt >= 1) { - cp.append(_elements.getFirst()); + final int size = classPathElements.size(); + if (size >= 1) { + cp.append(classPathElements.getFirst()); } - for (int i=1; i < cnt; i++) { + for (int i = 1; i < size; i++) { cp.append(File.pathSeparatorChar); - cp.append(_elements.get(i)); + cp.append(classPathElements.get(i)); } return cp.toString(); } public EXistClassLoader getClassLoader(ClassLoader parent) { - final URL urls[] = _elements - .stream() - .map(Path::toUri) - .map(u -> { - try { - return Optional.of(u.toURL()); - } catch(final MalformedURLException e) { - return Optional.empty(); - } - }).filter(Optional::isPresent) + final URL[] urls = classPathElements.stream() + .map(Path::toUri).map(u -> + { + try { + return Optional.of(u.toURL()); + } catch (final MalformedURLException e) { + return Optional.empty(); + } + }) + .filter(Optional::isPresent) .map(Optional::get) .toArray(URL[]::new); // try and ensure we have a classloader parent = or( - or( - or(Optional.ofNullable(parent), () -> Optional.ofNullable(Thread.currentThread().getContextClassLoader())), - () -> Optional.ofNullable(Classpath.class.getClassLoader()) - ), - () -> Optional.ofNullable(ClassLoader.getSystemClassLoader()) - ).orElse(null); + or( + or( + Optional.ofNullable(parent), + () -> Optional.ofNullable(Thread.currentThread().getContextClassLoader()) + ), + () -> Optional.ofNullable(Classpath.class.getClassLoader()) + ), + () -> Optional.ofNullable(ClassLoader.getSystemClassLoader()) + ) + .orElse(null); return new EXistClassLoader(urls, parent); } @Override public Iterator iterator() { - return _elements.iterator(); - } - - /** - * Copied from {@link com.evolvedbinary.j8fu.OptionalUtil#or(Optional, Supplier)} - * as org.exist.start is compiled into a separate Jar and doesn't have - * the rest of eXist available on the classpath - */ - private static Optional or(final Optional left, final Supplier> right) { - if(left.isPresent()) { - return left; - } else { - return right.get(); - } + return classPathElements.iterator(); } } diff --git a/exist-start/src/main/java/org/exist/start/EXistClassLoader.java b/exist-start/src/main/java/org/exist/start/classloader/EXistClassLoader.java similarity index 97% rename from exist-start/src/main/java/org/exist/start/EXistClassLoader.java rename to exist-start/src/main/java/org/exist/start/classloader/EXistClassLoader.java index 03964bc6374..dd1ed724653 100644 --- a/exist-start/src/main/java/org/exist/start/EXistClassLoader.java +++ b/exist-start/src/main/java/org/exist/start/classloader/EXistClassLoader.java @@ -19,7 +19,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.exist.start; +package org.exist.start.classloader; import java.net.MalformedURLException; import java.net.URL; diff --git a/exist-start/src/main/java/org/exist/start/classloader/ReflectionUtils.java b/exist-start/src/main/java/org/exist/start/classloader/ReflectionUtils.java new file mode 100644 index 00000000000..a6f1b00b8dc --- /dev/null +++ b/exist-start/src/main/java/org/exist/start/classloader/ReflectionUtils.java @@ -0,0 +1,83 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.start.classloader; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ReflectionUtils { + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private ReflectionUtils() { + // No instantiation allowed + } + + /** + * Invokes the main method of a specified class using the provided class loader and arguments. + *

+ * This method dynamically loads a class by its name, retrieves its {@code main} method, + * and then executes it with the given arguments. It is commonly used to launch a Java + * application from another Java program. + * + * @param classloader the {@link ClassLoader} to use for loading the class + * @param classname the fully qualified name of the class containing the main method + * @param args an array of {@code String} arguments to pass to the main method + * @throws IllegalAccessException if the main method is inaccessible + * @throws InvocationTargetException if an error occurs during the invocation of the main method + * @throws NoSuchMethodException if the main method cannot be found in the specified class + * @throws ClassNotFoundException if the specified class cannot be located using the given class loader + */ + public static void invokeMain(final ClassLoader classloader, final String classname, final String[] args) + throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException { + + final Class invokedClass = classloader.loadClass(classname); + + final Class[] methodParamTypes = new Class[1]; + methodParamTypes[0] = args.getClass(); + + final Method main = invokedClass.getDeclaredMethod("main", methodParamTypes); + + final Object[] methodParams = new Object[1]; + methodParams[0] = args; + main.invoke(null, methodParams); + } + + /** + * Obtains an instance of {@link EXistClassLoader}, sets it as the context class loader + * of the current thread, and returns it. + *

+ * This method creates a new instance of the {@link Classpath} class, retrieves an + * {@link EXistClassLoader} using the classpath, and assigns it as the thread's context + * class loader to ensure proper class loading behavior. + * + * @return an initialized {@link EXistClassLoader} instance that becomes the context + * class loader of the current thread + */ + public static EXistClassLoader getEXistClassLoader() { + final Classpath classpath = new Classpath(); + final EXistClassLoader eXistClassLoader = classpath.getClassLoader(null); + Thread.currentThread().setContextClassLoader(eXistClassLoader); + return eXistClassLoader; + } +} diff --git a/exist-start/src/test/java/org/exist/start/CompatibleJavaVersionCheckTest.java b/exist-start/src/test/java/org/exist/start/CompatibleJavaVersionCheckTest.java index e3af0d2efcc..17100dea3be 100644 --- a/exist-start/src/test/java/org/exist/start/CompatibleJavaVersionCheckTest.java +++ b/exist-start/src/test/java/org/exist/start/CompatibleJavaVersionCheckTest.java @@ -211,4 +211,14 @@ public void checkJava15_0_2() throws StartException { public void checkJava15_0_3() throws StartException { CompatibleJavaVersionCheck.checkForCompatibleJavaVersion(Optional.of("15.0.3")); } + + @Test + public void checkJava21() throws StartException { + CompatibleJavaVersionCheck.checkForCompatibleJavaVersion(Optional.of("21.0.6")); + } + + @Test + public void checkJava25() throws StartException { + CompatibleJavaVersionCheck.checkForCompatibleJavaVersion(Optional.of("25.0.1")); + } }