BaseClientBuilderOptions.java
/*
** Module : BaseClientBuilderOptions.java
** Abstract : store the parameters read from directory.
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- -------------------------------Description------------------------------------
** 001 MAG 20131128 First version.
** 002 MAG 20140128 Add WebSocket timeout.
** 003 MAG 20140204 Add Watchdog timeout.
** 004 CA 20140206 Refactored to make it unaware of the type of the P2J client being spawned.
** 005 GES 20150211 Removed application arguments support. Forced headless to be always present
** in JVM args. No other JVM arg is defaulted.
** 006 GES 20160203 Moved core initialization logic into a private method. Added a constructor
** for cloning, which doesn't call the core init but instead copies state from
** the given instance. This is useful for subclasses which need to avoid
** executing the initialization logic at runtime (e.g. because of security
** context constraints).
** 007 CA 20191211 When spawning a Java process programmatically, allow the caller to pass
** environment options specific to that call (beside any jvmArgs configured in
** the directory).
** 008 EVL 20200121 Adding directory based configurable debug level for spawner support. Adding
** support for extra jar files to be added to the classpath from directory.
** 009 VVT 20200213 initialize(): workarownd added: the -ea option can now be used in jvmArgs
** 010 OM 20200828 Added warning message.
** 011 SBI 20210922 Used getAvailablePorts to assign different ports for debugging and profiling
** 20210926 Moved "getAvailablePorts" logic to WebClientsManager.
** 012 SBI 20230411 Added getOverriddenOptions(), overrideClientOption, overrideWebClientPortOption.
** 013 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 014 SBI 20231218 Applied the debug profile if the web client starts the java debug agent, added
** parseDebuggerPort and parseJMXPort
** 015 GBB 20240102 Adding fwd-slf4j.jar to client process classpath.
** 016 EVL 20240118 Adding method to check if we need native logger for spawn.
** 017 CA 20240210 'clientConfig/properties' contains a map of operating system variables to be passed to the
** spawned process.
** 018 SBI 20240208 Added suspend option support.
** RFB 20240209 Made changes in handling of suspend option for full support. Ref. #7824.
** 019 TJD 20240606 Removing fwd-slf4j.jar from client process classpath, its not needed anymore since #6692
** 020 GBB 20240709 Hard-coded config names replaced by ConfigItem constants. Moving irrelevant fields
** and methods out of the class for better cohesion. Making all fields but
** runtimeJvmArguments immutable. Removing the mechanics of copying the object.
** 020 GBB 20240718 Config read implemented in concrete class.
** 021 CA 20240722 Fixed reading of Map configurations. Changed the 'clientConfig/properties' to be a real
** map.
** 022 CA 20240730 resolveEnvVars may retrieve an unmodifiable map, make sure it's modifiable.
** 023 AP 20250303 Added timeout for spawner launching.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.main;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.logging.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
/**
* The parameters stored inside this structure are read from directory and are used as default
* parameters to create and spawn a P2J client process.
*/
public abstract class BaseClientBuilderOptions
{
/** Logger. */
private static final CentralLogger LOG = CentralLogger.get(BaseClientBuilderOptions.class.getName());
/** Default command. */
private static final String DEFAULT_COMMAND = "java";
/** Main class, must have a main method */
private static final Class<?> MAIN_CLASS = ClientDriver.class;
/** Runtime calculated path to our main jar file. */
private static final String DEFAULT_PATH = getDefaultPath();
/** Default working directory. */
private static final String DEFAULT_WORK_DIR = "./";
/** The search pattern for the debugger suspend option */
private final static Pattern DEBUGGER_SUSPEND_PATTERN = Pattern.compile("suspend=([ny])");
/** Secure socket flag. */
private final boolean secure;
/** Java launcher executable name. */
private final String command;
/** User's working directory. */
private final String workingDirectory;
/** Class path. */
private final String classPath;
/** Native library path. */
private final String libPath;
/** Spawner executable name. */
private final String spawner;
/** Client configuration file. */
private final String configFile;
/** Using optional logging in native spawn code. */
private final boolean spawnerDebugEnabled;
/** JVM arguments. */
private final List<String> defaultJvmArguments;
/** JVM arguments overrides. */
private final ThreadLocal<List<String>> runtimeJvmArguments;
/** Storage for debugger suspend option */
private final String suspendOption;
/** The operating system environment variables to be set at the spawned OS process. */
private final Map<String, String> envProperties;
/** Unmodifiable list of P2J client bootstrap configs */
private final List<String> cfgOverrides;
/** Launch timeout for the spawner, expressed in seconds */
private final Integer spawnerLaunchTimeout;
/**
* Initialize. Read all configuration parameters from directory.
*
* @param env
* Map of additional environment properties, in the <code>key=value</code> form.
*/
protected BaseClientBuilderOptions(Map<String, String> env)
{
// TODO: should not support insecure mode
secure = read(ConfigItem.SECURE, true);
command = quotedString(read(ConfigItem.COMMAND, DEFAULT_COMMAND));
libPath = quotedString(read(ConfigItem.LIB_PATH, DEFAULT_PATH));
spawner = quotedString(read(ConfigItem.SPAWNER, DEFAULT_PATH + "spawn"));
spawnerDebugEnabled = read(ConfigItem.SPAWNER_DEBUG, false);
workingDirectory = quotedString(read(ConfigItem.WORKING_DIR, DEFAULT_WORK_DIR));
configFile = quotedString(read(ConfigItem.CONFIG_FILE, null));
classPath = addJarsToClasspath();
String jvm = read(ConfigItem.JVM_ARGS, "");
suspendOption = parseDebuggerSuspendOption(jvm, "n");
defaultJvmArguments = resolveJvmArgs(jvm);
envProperties = resolveEnvVars(env);
String bootstrapCfg = readCfgOverrides("");
List<String> cfgOverrides = new LinkedList<>(Arrays.asList(bootstrapCfg.split(" ")));
this.cfgOverrides = Collections.unmodifiableList(cfgOverrides);
this.runtimeJvmArguments = new ThreadLocal<List<String>>()
{
@Override public List<String> initialValue() {
ArrayList<String> resetJvmArgs = new ArrayList<>();
// addAll instead of using the constructor of ArrayList, because defaultJvmArguments is unmodifiable
resetJvmArgs.addAll(defaultJvmArguments);
return resetJvmArgs;
}
};
this.spawnerLaunchTimeout = read(ConfigItem.SPAWNER_LAUNCH_TIMEOUT, null);
}
/**
* Resets the thread local value of {@link #runtimeJvmArguments}.
*/
public void reset()
{
ArrayList<String> resetJvmArgs = new ArrayList<>();
// addAll instead of using the constructor of ArrayList, because defaultJvmArguments is unmodifiable
resetJvmArgs.addAll(defaultJvmArguments);
runtimeJvmArguments.set(resetJvmArgs);
}
/**
* Get the {@link #envProperties}.
*
* @return See above.
*/
public Map<String, String> getEnvProperties()
{
return envProperties;
}
/**
* Get the security mode.
*
* @return <code>true</code> use a secured connection <code>false</code> otherwise.
*/
public boolean isSecure()
{
return secure;
}
/**
* Get command
*
* @return Command name string.
*/
public String getCommand()
{
return command;
}
/**
* Get classpath
*
* @return Class path string.
*/
public String getClassPath()
{
return classPath;
}
/**
* Get native library path.
*
* @return Native library path string.
*/
public String getLibPath()
{
return libPath;
}
/**
* Get user working directory.
*
* @return User working directory.
*/
public String getWorkingDirectory()
{
return workingDirectory;
}
/**
* Get configuration file name.
*
* @return Configuration file name.
*/
public String getConfigFile()
{
return configFile;
}
/**
* Get JVM arguments
*
* @return A List with JVM arguments.
*/
public List<String> getJvmArguments()
{
return runtimeJvmArguments.get();
}
/**
* Get spawner command.
*
* @return The spawner command;
*/
public String getSpawner()
{
return spawner;
}
/**
* Get the Java process main class.
*
* @return The main class.
*/
public Class<?> getMainClass()
{
return MAIN_CLASS;
}
/**
* Check if native log option is required.
*
* @return If <code>true</code> use a extra logger in native code <code>false</code> otherwise.
*/
public boolean isSpawnerDebugEnabled()
{
return spawnerDebugEnabled;
}
/**
* Returns the debugger suspend option.
*
* @return See above.
*/
public String getSuspendOption()
{
return suspendOption;
}
/**
* Get P2J client's bootstrap config.
*
* @return A List with bootstrap config settings.
*/
public List<String> getCfgOverrides()
{
return cfgOverrides;
}
/**
* When the string contains space characters a quoted string is build.
* On Windows OS spaces could be used in file path, file name, font name and other
* strings. In order to use such kind of strings in API calls the string should be
* transformed in quoted form.
*
* @param text Text to search for spaces.
*
* @return If no spaces found return the initial string otherwise return a quoted string.
*/
static String quotedString(String text)
{
if (text == null)
{
return null;
}
if (text.startsWith("\"") && text.endsWith("\""))
{
return text;
}
return (text.contains(" ")) ? "\\\"" + text +"\\\"" : text;
}
/**
* Returns the spawner launch timeout, expressed in seconds.
*
* @return See above.
*/
public Integer getSpawnerLaunchTimeout()
{
return spawnerLaunchTimeout;
}
/**
* Update jvmArguments with new key and value pair.
*
* @param key
* The key of a target jvm argument
* @param value
* Its new value
*/
void updateJvmArgument(String key, String value)
{
List<String> jvmArgs = runtimeJvmArguments.get();
String keyWithAssignmentPrefix = key + "=";
for(int i = 0; i < jvmArgs.size(); i++)
{
String arg = jvmArgs.get(i);
if (arg.startsWith(keyWithAssignmentPrefix))
{
jvmArgs.set(i, keyWithAssignmentPrefix + value);
}
}
}
/**
* A convenience method to read the client config with the specified default value.
*
* @param clientConfig
* The client config to read.
* @param defaultValue
* The default value returned if no value found in directory.
* @param <T>
* The type of the client config.
*
* @return The client config value as read from directory.
*/
abstract <T> T read(ConfigItem<T> clientConfig, T defaultValue);
/**
* A convenience method to read the client config with the specified default value.
*
* @param clientConfig
* The client config to read.
* @param defaultValue
* The default value returned if no value found in directory.
* @param placeholderValue
* The value to replace the placeholder in the directory node name for the config.
* @param <T>
* The type of the client config.
*
* @return The client config value as read from directory.
*/
abstract <T> T read(ConfigItem<T> clientConfig, T defaultValue, String placeholderValue);
/**
* Reads and returns the value of clientConfig/cfgOverrides.
*
* @param defaultValue
* The default value returned if no value found in directory.
*
* @return The cfgOverrides value.
*/
abstract String readCfgOverrides(String defaultValue);
/**
* Get the debugger suspend option from the jvm arguments string, otherwise return null.
*
* @param jvmArgs
* The jvm arguments string
* @param defaultValue
* The default value
*
* @return The debugger suspend option or null value.
*/
private String parseDebuggerSuspendOption(String jvmArgs, String defaultValue)
{
Matcher m = DEBUGGER_SUSPEND_PATTERN.matcher(jvmArgs);
if (m.find())
{
return m.group(1);
}
return defaultValue;
}
/**
* Get default path as the p2j.jar file location.
*
* @return Path to p2j.jar file.
*/
private static String getDefaultPath()
{
String message = "Could not determine the location of the p2j.jar file.";
String defaultPath = "./";
try
{
URL url = MAIN_CLASS.getProtectionDomain().getCodeSource().getLocation();
String classPath = url.toURI().getPath();
File jarFile = new File(classPath);
// if the source is in a folder and not jar, then this can cause problems
if (!jarFile.exists())
{
LOG.logp(Level.WARNING, "ClientBuilderOptions", "getDefaultPath", message);
}
else
{
// default path
defaultPath = jarFile.getParentFile().getPath();
if (!defaultPath.endsWith(File.separator))
{
defaultPath += File.separator;
}
}
}
catch (URISyntaxException e)
{
LOG.logp(Level.WARNING, "ClientBuilderOptions", "getDefaultPath", message, e);
}
return defaultPath;
}
/**
* Reads the classpath and the extra jars from directory, appends each entry to the default path and
* checks if the file exists before compiling the final classpath.
*
* @return The verified classpath.
*/
private String addJarsToClasspath()
{
String cp = quotedString(read(ConfigItem.CLASSPATH, DEFAULT_PATH + "p2j.jar"));
String cpExtraJars = quotedString(read(ConfigItem.CLASSPATH_EXTRA_JARS, null));
// check if extra jars can be added to the classpath
if (cpExtraJars != null && cpExtraJars.length() > 1)
{
String[] jars = cpExtraJars.split(",");
StringBuilder sb = new StringBuilder();
sb.append(cp);
for (int i = 0; i < jars.length; i++)
{
String extraJar = DEFAULT_PATH + jars[i] + ".jar";
File jarFile = new File(extraJar);
if (jarFile.exists())
{
sb.append(File.pathSeparator);
sb.append(extraJar);
}
else
{
LOG.warning("Failed to add '" + jars[i] + "' library to class path. " +
"Cause: file not found('" + extraJar + "')");
}
}
cp = sb.toString();
}
return cp;
}
/**
* Reads the map of env vars from directory and merges it with the map of runtime env vars. Returns an
* unmodifiable map.
*
* @param env
* The map of env vars.
*
* @return The final map of env vars, unmodifiable.
*/
private Map<String, String> resolveEnvVars(Map<String, String> env)
{
Map<String, String> envVars = read(ConfigItem.ENV_PROPERTIES, null);
if (env != null)
{
// explicit environment properties have precedence over the ones under clientConfig/properties
// ensure that the map is modifiable
envVars = new LinkedHashMap<>(envVars);
envVars.putAll(env);
}
return Collections.unmodifiableMap(envVars);
}
/**
* Parses the JVM arguments line from directory, adds the necessary 'headless' argument if missing and
* returns the arguments as an unmodifiable list.
*
* @param jvmConfig
* The JVM arguments as read from directory.
*
* @return The final list of JVM arguments, unmodifiable.
*/
private List<String> resolveJvmArgs(String jvmConfig)
{
// we always must pass headless mode
if (!jvmConfig.contains("-Djava.awt.headless=true"))
{
String pad = (jvmConfig.length() == 0) ? "" : " ";
jvmConfig = jvmConfig + pad + "-Djava.awt.headless=true";
}
List<String> jvmArgs = new LinkedList<>();
String[] args = jvmConfig.split("(?=-[XDe])");
for (int i = 0; i < args.length; i++)
{
jvmArgs.add(args[i].trim());
}
return Collections.unmodifiableList(jvmArgs);
}
}