diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/TokenReplace.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/TokenReplace.java
new file mode 100644
index 0000000000000..7c213851b9bf7
--- /dev/null
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/TokenReplace.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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 General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.jpackage.internal.util;
+
+import static java.util.stream.Collectors.joining;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+/**
+ * Class to replace tokens in strings.
+ *
+ * Single instance holds a list of tokens. Tokens can be substrings of each other.
+ * The implementation performs greedy replacement: longer tokens are replaced first.
+ */
+public final class TokenReplace {
+
+ private record TokenCut(String[] main, String[] sub) {
+ static String[] orderTokens(String... tokens) {
+ if (tokens.length == 0) {
+ throw new IllegalArgumentException("Empty token list");
+ }
+
+ final var orderedTokens = Stream.of(tokens)
+ .sorted(Comparator.naturalOrder().thenComparing(Comparator.comparingInt(String::length)))
+ .distinct()
+ .toArray(String[]::new);
+
+ if (orderedTokens[0].isEmpty()) {
+ throw new IllegalArgumentException("Empty token in the list of tokens");
+ }
+
+ return orderedTokens;
+ }
+
+ static TokenCut createFromOrderedTokens(String... tokens) {
+ final List subTokens = new ArrayList<>();
+
+ for (var i = 0; i < tokens.length - 1; ++i) {
+ final var x = tokens[i];
+ for (var j = i + 1; j < tokens.length; ++j) {
+ final var y = tokens[j];
+ if (y.contains(x)) {
+ subTokens.add(i);
+ }
+ }
+ }
+
+ if (subTokens.isEmpty()) {
+ return new TokenCut(tokens, null);
+ } else {
+ final var main = IntStream.range(0, tokens.length)
+ .mapToObj(Integer::valueOf)
+ .filter(Predicate.not(subTokens::contains))
+ .map(i -> {
+ return tokens[i];
+ }).toArray(String[]::new);
+ final var sub = subTokens.stream().map(i -> {
+ return tokens[i];
+ }).toArray(String[]::new);
+ return new TokenCut(main, sub);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("TokenCut(main=%s, sub=%s)", Arrays.toString(main), Arrays.toString(sub));
+ }
+ }
+
+ public TokenReplace(String... tokens) {
+ tokens = TokenCut.orderTokens(tokens);
+
+ this.tokens = tokens;
+ regexps = new ArrayList<>();
+
+ for (;;) {
+ final var tokenCut = TokenCut.createFromOrderedTokens(tokens);
+ regexps.add(Pattern.compile(Stream.of(tokenCut.main()).map(Pattern::quote).collect(joining("|", "(", ")"))));
+
+ if (tokenCut.sub() == null) {
+ break;
+ }
+
+ tokens = tokenCut.sub();
+ }
+ }
+
+ public String applyTo(String str, Function tokenValueSupplier) {
+ Objects.requireNonNull(str);
+ Objects.requireNonNull(tokenValueSupplier);
+ for (final var regexp : regexps) {
+ str = regexp.matcher(str).replaceAll(mr -> {
+ final var token = mr.group();
+ return Matcher.quoteReplacement(Objects.requireNonNull(tokenValueSupplier.apply(token), () -> {
+ return String.format("Null value for token [%s]", token);
+ }).toString());
+ });
+ }
+ return str;
+ }
+
+ public String recursiveApplyTo(String str, Function tokenValueSupplier) {
+ String newStr;
+ int counter = tokens.length + 1;
+ while (!(newStr = applyTo(str, tokenValueSupplier)).equals(str)) {
+ str = newStr;
+ if (counter-- == 0) {
+ throw new IllegalStateException("Infinite recursion");
+ }
+ }
+ return newStr;
+ }
+
+ @Override
+ public int hashCode() {
+ // Auto generated code
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + Arrays.hashCode(tokens);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // Auto generated code
+ if (this == obj) {
+ return true;
+ }
+ if ((obj == null) || (getClass() != obj.getClass())) {
+ return false;
+ }
+ TokenReplace other = (TokenReplace) obj;
+ return Arrays.equals(tokens, other.tokens);
+ }
+
+ @Override
+ public String toString() {
+ return "TokenReplace(" + String.join("|", tokens) + ")";
+ }
+
+ public static TokenReplace combine(TokenReplace x, TokenReplace y) {
+ return new TokenReplace(Stream.of(x.tokens, y.tokens).flatMap(Stream::of).toArray(String[]::new));
+ }
+
+ public static Function createCachingTokenValueSupplier(Map> tokenValueSuppliers) {
+ Objects.requireNonNull(tokenValueSuppliers);
+ final Map cache = new HashMap<>();
+ return token -> {
+ final var value = cache.computeIfAbsent(token, k -> {
+ final var tokenValueSupplier = Objects.requireNonNull(tokenValueSuppliers.get(token), () -> {
+ return String.format("No token value supplier for token [%s]", token);
+ });
+ return Optional.ofNullable(tokenValueSupplier.get()).orElse(NULL_SUPPLIED);
+ });
+
+ if (value == NULL_SUPPLIED) {
+ throw new NullPointerException(String.format("Null value for token [%s]", token));
+ }
+
+ return value;
+ };
+ }
+
+ private final String[] tokens;
+ private final transient List regexps;
+ private final static Object NULL_SUPPLIED = new Object();
+}
diff --git a/src/jdk.jpackage/share/native/applauncher/AppLauncher.cpp b/src/jdk.jpackage/share/native/applauncher/AppLauncher.cpp
index 96e8885e6ab50..fd1d07e92e05a 100644
--- a/src/jdk.jpackage/share/native/applauncher/AppLauncher.cpp
+++ b/src/jdk.jpackage/share/native/applauncher/AppLauncher.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -164,9 +164,9 @@ CfgFile* AppLauncher::createCfgFile() const {
<< cfgFilePath << "\"");
CfgFile::Macros macros;
- macros[_T("$APPDIR")] = appDirPath;
- macros[_T("$BINDIR")] = FileUtils::dirname(launcherPath);
- macros[_T("$ROOTDIR")] = imageRoot;
+ macros[_T("APPDIR")] = appDirPath;
+ macros[_T("BINDIR")] = FileUtils::dirname(launcherPath);
+ macros[_T("ROOTDIR")] = imageRoot;
std::unique_ptr dummy(new CfgFile());
CfgFile::load(cfgFilePath).expandMacros(macros).swap(*dummy);
return dummy.release();
diff --git a/src/jdk.jpackage/share/native/applauncher/CfgFile.cpp b/src/jdk.jpackage/share/native/applauncher/CfgFile.cpp
index b5049334b6482..0e99d131e69ec 100644
--- a/src/jdk.jpackage/share/native/applauncher/CfgFile.cpp
+++ b/src/jdk.jpackage/share/native/applauncher/CfgFile.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -25,13 +25,16 @@
#include "kludge_c++11.h"
+#include
#include
#include
#include "CfgFile.h"
+#include "SysInfo.h"
#include "Log.h"
#include "Toolbox.h"
#include "FileUtils.h"
#include "ErrorHandling.h"
+#include "StringProcessing.h"
const CfgFile::Properties& CfgFile::getProperties(
@@ -60,38 +63,82 @@ CfgFile& CfgFile::setPropertyValue(const SectionName& sectionName,
namespace {
-tstring expandMacros(const tstring& str, const CfgFile::Macros& macros) {
- tstring reply = str;
- CfgFile::Macros::const_iterator it = macros.begin();
- const CfgFile::Macros::const_iterator end = macros.end();
- for (; it != end; ++it) {
- reply = tstrings::replace(reply, it->first, it->second);
+template
+void iterateProperties(CfgFileType& cfgFile, OpType& op) {
+ for (auto mapIt = cfgFile.begin(), mapEnd = cfgFile.end(); mapIt != mapEnd; ++mapIt) {
+ for (auto propertyIt = mapIt->second.begin(), propertyEnd = mapIt->second.end(); propertyIt != propertyEnd; ++propertyIt) {
+ for (auto strIt = propertyIt->second.begin(), strEnd = propertyIt->second.end(); strIt != strEnd; ++strIt) {
+ op(strIt);
+ }
+ }
}
- return reply;
}
+struct tokenize_strings {
+ void operator () (tstring_array::const_iterator& strIt) {
+ const auto tokens = StringProcessing::tokenize(*strIt);
+ values.push_back(tokens);
+ }
+
+ std::set variableNames() const {
+ std::set allVariableNames;
+ for (auto it = values.begin(), end = values.end(); it != end; ++it) {
+ const auto variableNames = StringProcessing::extractVariableNames(*it);
+ allVariableNames.insert(variableNames.begin(), variableNames.end());
+ }
+
+ return allVariableNames;
+ }
+
+ std::vector values;
+};
+
+class expand_macros {
+public:
+ expand_macros(const CfgFile::Macros& m,
+ std::vector::iterator iter) : macros(m), curTokenizedString(iter) {
+ }
+
+ void operator () (tstring_array::iterator& strIt) {
+ StringProcessing::expandVariables(*curTokenizedString, macros);
+ auto newStr = StringProcessing::stringify(*curTokenizedString);
+ ++curTokenizedString;
+ if (*strIt != newStr) {
+ LOG_TRACE(tstrings::any() << "Map [" << *strIt << "] into [" << newStr << "]");
+ }
+ strIt->swap(newStr);
+ }
+
+private:
+ const CfgFile::Macros& macros;
+ std::vector::iterator curTokenizedString;
+};
+
} // namespace
CfgFile CfgFile::expandMacros(const Macros& macros) const {
CfgFile copyCfgFile = *this;
- PropertyMap::iterator mapIt = copyCfgFile.data.begin();
- const PropertyMap::iterator mapEnd = copyCfgFile.data.end();
- for (; mapIt != mapEnd; ++mapIt) {
- Properties::iterator propertyIt = mapIt->second.begin();
- const Properties::iterator propertyEnd = mapIt->second.end();
- for (; propertyIt != propertyEnd; ++propertyIt) {
- tstring_array::iterator strIt = propertyIt->second.begin();
- const tstring_array::iterator strEnd = propertyIt->second.end();
- for (; strIt != strEnd; ++strIt) {
- tstring newValue;
- while ((newValue = ::expandMacros(*strIt, macros)) != *strIt) {
- strIt->swap(newValue);
- }
+ tokenize_strings tokenizedStrings;
+ iterateProperties(static_cast(copyCfgFile.data), tokenizedStrings);
+
+ Macros allMacros(macros);
+
+ const auto variableNames = tokenizedStrings.variableNames();
+ for (auto it = variableNames.begin(), end = variableNames.end(); it != end; ++it) {
+ if (macros.find(*it) == macros.end()) {
+ // Not one of the reserved macro names. Assuming an environment variable.
+ const auto envVarName = *it;
+ if (SysInfo::isEnvVariableSet(envVarName)) {
+ const auto envVarValue = SysInfo::getEnvVariable(envVarName);
+ allMacros[envVarName] = envVarValue;
}
}
}
+ expand_macros expandMacros(allMacros, tokenizedStrings.values.begin());
+ iterateProperties(copyCfgFile.data, expandMacros);
+
return copyCfgFile;
}
diff --git a/src/jdk.jpackage/share/native/applauncher/CfgFile.h b/src/jdk.jpackage/share/native/applauncher/CfgFile.h
index 03f70d94fde06..b1edea237a3ea 100644
--- a/src/jdk.jpackage/share/native/applauncher/CfgFile.h
+++ b/src/jdk.jpackage/share/native/applauncher/CfgFile.h
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -93,7 +93,7 @@ class CfgFile {
typedef std::map Macros;
/**
- * Returns copy of this instance with the given macros expanded.
+ * Returns copy of this instance with the given macros and environment variables expanded.
*/
CfgFile expandMacros(const Macros& macros) const;
diff --git a/src/jdk.jpackage/share/native/applauncher/StringProcessing.cpp b/src/jdk.jpackage/share/native/applauncher/StringProcessing.cpp
new file mode 100644
index 0000000000000..207db25fd94ba
--- /dev/null
+++ b/src/jdk.jpackage/share/native/applauncher/StringProcessing.cpp
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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 General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+#include "kludge_c++11.h"
+
+#include
+#include "StringProcessing.h"
+
+
+namespace StringProcessing {
+
+namespace {
+
+class TokenBuilder {
+public:
+ TokenBuilder(const tstring& v): cur(v.c_str()) {
+ }
+
+ void addNextToken(tstring::const_pointer end, TokenType type, TokenizedString& tokens) {
+ if (end != cur) {
+ const auto value = tstring(cur, end - cur);
+ cur = end;
+ tokens.push_back(Token(type, value));
+ }
+ }
+
+private:
+ tstring::const_pointer cur;
+};
+
+bool isValidVariableFirstChar(tstring::value_type chr) {
+ if ('A' <= chr && chr <= 'Z') {
+ return true;
+ } else if ('a' <= chr && chr <= 'z') {
+ return true;
+ } else if ('_' == chr) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool isValidVariableOtherChar(tstring::value_type chr) {
+ if (isValidVariableFirstChar(chr)) {
+ return true;
+ } else if ('0' <= chr && chr <= '9') {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+} // namespace
+
+TokenizedString tokenize(const tstring& str) {
+ TokenizedString tokens;
+ TokenBuilder tb(str);
+ tstring::const_pointer cur = str.c_str();
+ const tstring::const_pointer end = cur + str.length();
+ while (cur != end) {
+ if (*cur == '\\' && cur + 1 != end) {
+ const auto maybeNextToken = cur++;
+ if (*cur == '\\' || *cur == '$') {
+ tb.addNextToken(maybeNextToken, STRING, tokens);
+ tb.addNextToken(++cur, ESCAPED_CHAR, tokens);
+ continue;
+ }
+ } else if (*cur == '$' && cur + 1 != end) {
+ const auto maybeNextToken = cur++;
+ bool variableFound = false;
+ if (*cur == '{') {
+ do {
+ cur++;
+ } while (cur != end && *cur != '}');
+ if (cur != end) {
+ variableFound = true;
+ cur++;
+ }
+ } else if (isValidVariableFirstChar(*cur)) {
+ variableFound = true;
+ do {
+ cur++;
+ } while (cur != end && isValidVariableOtherChar(*cur));
+ } else {
+ continue;
+ }
+ if (variableFound) {
+ tb.addNextToken(maybeNextToken, STRING, tokens);
+ tb.addNextToken(cur, VARIABLE, tokens);
+ }
+ } else {
+ ++cur;
+ }
+ }
+ tb.addNextToken(cur, STRING, tokens);
+ return tokens;
+}
+
+
+tstring stringify(const TokenizedString& tokens) {
+ tstringstream ss;
+ TokenizedString::const_iterator it = tokens.begin();
+ const TokenizedString::const_iterator end = tokens.end();
+ for (; it != end; ++it) {
+ if (it->type() == ESCAPED_CHAR) {
+ ss << it->value().substr(1);
+ } else {
+ ss << it->value();
+ }
+ }
+ return ss.str();
+}
+
+
+namespace {
+
+tstring getVariableName(const tstring& str) {
+ if (tstrings::endsWith(str, _T("}"))) {
+ // ${VAR}
+ return str.substr(2, str.length() - 3);
+ } else {
+ // $VAR
+ return str.substr(1);
+ }
+}
+
+} // namespace
+
+VariableNameList extractVariableNames(const TokenizedString& tokens) {
+ VariableNameList reply;
+
+ TokenizedString::const_iterator it = tokens.begin();
+ const TokenizedString::const_iterator end = tokens.end();
+ for (; it != end; ++it) {
+ if (it->type() == VARIABLE) {
+ reply.push_back(getVariableName(it->value()));
+ }
+ }
+
+ std::sort(reply.begin(), reply.end());
+ reply.erase(std::unique(reply.begin(), reply.end()), reply.end());
+ return reply;
+}
+
+
+void expandVariables(TokenizedString& tokens, const VariableValues& variableValues) {
+ TokenizedString::iterator it = tokens.begin();
+ const TokenizedString::iterator end = tokens.end();
+ for (; it != end; ++it) {
+ if (it->type() == VARIABLE) {
+ const auto entry = variableValues.find(getVariableName(it->value()));
+ if (entry != variableValues.end()) {
+ auto newToken = Token(STRING, entry->second);
+ std::swap(*it, newToken);
+ }
+ }
+ }
+}
+
+} // namespace StringProcessing
diff --git a/src/jdk.jpackage/share/native/applauncher/StringProcessing.h b/src/jdk.jpackage/share/native/applauncher/StringProcessing.h
new file mode 100644
index 0000000000000..cdd1120d62060
--- /dev/null
+++ b/src/jdk.jpackage/share/native/applauncher/StringProcessing.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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 General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+
+#ifndef StringProcessor_h
+#define StringProcessor_h
+
+#include