This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 36120373e787 CAMEL-25098: camel-main - Bootstrap, profiles and
duration options: fix bugs found in a deep review (#27000)
36120373e787 is described below
commit 36120373e787d0681972503109fc63a795eff019
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 28 22:42:33 2026 +0200
CAMEL-25098: camel-main - Bootstrap, profiles and duration options: fix
bugs found in a deep review (#27000)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
core/camel-main/pom.xml | 6 +
.../org/apache/camel/main/BaseMainSupport.java | 134 ++++++-----
.../apache/camel/main/MainCommandLineSupport.java | 9 +-
.../camel/main/MainDurationEventNotifier.java | 33 ++-
.../java/org/apache/camel/main/MainSupport.java | 7 +-
.../org/apache/camel/main/ProfileConfigurer.java | 22 +-
.../camel/main/MainBootstrapEdgeCasesTest.java | 248 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 17 ++
8 files changed, 404 insertions(+), 72 deletions(-)
diff --git a/core/camel-main/pom.xml b/core/camel-main/pom.xml
index a0b143098bec..038ced54446c 100644
--- a/core/camel-main/pom.xml
+++ b/core/camel-main/pom.xml
@@ -155,6 +155,12 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <version>${awaitility-version}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
diff --git
a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
index 0f9c0b90decd..784b83aea7b3 100644
--- a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
+++ b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
@@ -602,6 +602,23 @@ public abstract class BaseMainSupport extends BaseService {
mainConfigurationProperties.setRoutesIncludePattern(value);
return null;
});
+ // the options for the routes collector are also needed for
scanning the DSL sources for modeline
+ autoConfigurationSingleOption(camelContext,
autoConfiguredProperties, "camel.main.routesExcludePattern",
+ value -> {
+
mainConfigurationProperties.setRoutesExcludePattern(value);
+ return null;
+ });
+ autoConfigurationSingleOption(camelContext,
autoConfiguredProperties, "camel.main.routesCollectorEnabled",
+ value -> {
+
mainConfigurationProperties.setRoutesCollectorEnabled(Boolean.parseBoolean(value));
+ return null;
+ });
+ autoConfigurationSingleOption(camelContext,
autoConfiguredProperties,
+ "camel.main.routesCollectorIgnoreLoadingError",
+ value -> {
+
mainConfigurationProperties.setRoutesCollectorIgnoreLoadingError(Boolean.parseBoolean(value));
+ return null;
+ });
recorder.endStep(step);
if (mainConfigurationProperties.isModeline()) {
@@ -1136,7 +1153,7 @@ public abstract class BaseMainSupport extends BaseService
{
mainConfigurationProperties.setAutoConfigurationSystemPropertiesEnabled(
CamelContextHelper.parseBoolean(camelContext,
jvmEnabled.toString()));
String loc =
prop.getLocation("camel.main.autoConfigurationSystemPropertiesEnabled");
- autoConfiguredProperties.put(loc,
"camel.autoConfigurationSystemPropertiesEnabled",
+ autoConfiguredProperties.put(loc,
"camel.main.autoConfigurationSystemPropertiesEnabled",
jvmEnabled.toString());
}
@@ -1145,7 +1162,7 @@ public abstract class BaseMainSupport extends BaseService
{
if
(mainConfigurationProperties.isAutoConfigurationEnvironmentVariablesEnabled()) {
propENV = MainHelper.loadEnvironmentVariablesAsProperties(new
String[] { "camel.main." });
if (!propENV.isEmpty()) {
- prop.putAll(propENV);
+ putAllOverride(prop, "ENV", propENV);
LOG.debug("Properties from OS environment variables:");
for (String key : propENV.stringPropertyNames()) {
LOG.debug(" {}={}", key, propENV.getProperty(key));
@@ -1157,7 +1174,7 @@ public abstract class BaseMainSupport extends BaseService
{
if
(mainConfigurationProperties.isAutoConfigurationSystemPropertiesEnabled()) {
propJVM = MainHelper.loadJvmSystemPropertiesAsProperties(new
String[] { "camel.main." });
if (!propJVM.isEmpty()) {
- prop.putAll(propJVM);
+ putAllOverride(prop, "SYS", propJVM);
LOG.debug("Properties from JVM system properties:");
for (String key : propJVM.stringPropertyNames()) {
LOG.debug(" {}={}", key, propJVM.getProperty(key));
@@ -1166,22 +1183,11 @@ public abstract class BaseMainSupport extends
BaseService {
}
// special for fail-fast as we need to know this early before we set
all the other options
- String loc = "ENV";
- Object failFast = propENV != null ?
propENV.remove("camel.main.autoconfigurationfailfast") : null;
- if (ObjectHelper.isNotEmpty(propJVM)) {
- Object val =
propJVM.remove("camel.main.autoconfigurationfailfast");
- if (ObjectHelper.isNotEmpty(val)) {
- loc = "SYS";
- failFast = val;
- }
- }
- if (ObjectHelper.isNotEmpty(failFast)) {
- mainConfigurationProperties
-
.setAutoConfigurationFailFast(CamelContextHelper.parseBoolean(camelContext,
failFast.toString()));
- autoConfiguredProperties.put(loc,
"camel.main.autoConfigurationFailFast", failFast.toString());
- } else {
- loc = prop.getLocation("camel.main.autoConfigurationFailFast");
- failFast = prop.remove("camel.main.autoConfigurationFailFast");
+ // (SYS and ENV have been merged with priority, and the key can be in
any case such as from ENV)
+ String key = findKeyIgnoreCase(prop,
"camel.main.autoConfigurationFailFast");
+ if (key != null) {
+ String loc = prop.getLocation(key);
+ Object failFast = prop.remove(key);
if (ObjectHelper.isNotEmpty(failFast)) {
mainConfigurationProperties
.setAutoConfigurationFailFast(CamelContextHelper.parseBoolean(camelContext,
failFast.toString()));
@@ -1190,12 +1196,46 @@ public abstract class BaseMainSupport extends
BaseService {
}
}
+ /**
+ * Puts all the properties from the given source (such as ENV or SYS),
which override any existing property with the
+ * same key in another case (such as camel.main.shutdowntimeout from ENV
and camel.main.shutdownTimeout).
+ */
+ private static void putAllOverride(OrderedLocationProperties prop, String
loc, Properties source) {
+ for (String key : source.stringPropertyNames()) {
+ String norm = normalizeKey(key);
+ List<String> existing = new ArrayList<>();
+ for (String k : prop.stringPropertyNames()) {
+ if (!k.equals(key) && normalizeKey(k).equals(norm)) {
+ existing.add(k);
+ }
+ }
+ existing.forEach(prop::remove);
+ prop.put(loc, key, source.getProperty(key));
+ }
+ }
+
+ private static String findKeyIgnoreCase(OrderedLocationProperties prop,
String key) {
+ String norm = normalizeKey(key);
+ for (String k : prop.stringPropertyNames()) {
+ if (normalizeKey(k).equals(norm)) {
+ return k;
+ }
+ }
+ return null;
+ }
+
+ private static String normalizeKey(String key) {
+ // keys with map or list keys are kept as-is
+ if (key.indexOf('[') != -1) {
+ return key;
+ }
+ return key.toLowerCase(Locale.ENGLISH).replace("-", "");
+ }
+
protected void autoConfigurationSingleOption(
CamelContext camelContext, OrderedLocationProperties
autoConfiguredProperties,
String optionName, Function<String, Object> setter) {
- String lowerOptionName = optionName.toLowerCase(Locale.US);
-
// load properties
OrderedLocationProperties prop = (OrderedLocationProperties)
camelContext.getPropertiesComponent()
.loadProperties(name -> name.startsWith("camel."),
MainHelper::optionKey);
@@ -1208,7 +1248,7 @@ public abstract class BaseMainSupport extends BaseService
{
if
(mainConfigurationProperties.isAutoConfigurationEnvironmentVariablesEnabled()) {
propENV = MainHelper.loadEnvironmentVariablesAsProperties(new
String[] { "camel.main." });
if (!propENV.isEmpty()) {
- prop.putAll("ENV", propENV);
+ putAllOverride(prop, "ENV", propENV);
LOG.debug("Properties from OS environment variables:");
for (String key : propENV.stringPropertyNames()) {
LOG.debug(" {}={}", key, propENV.getProperty(key));
@@ -1220,28 +1260,20 @@ public abstract class BaseMainSupport extends
BaseService {
if
(mainConfigurationProperties.isAutoConfigurationSystemPropertiesEnabled()) {
propJVM = MainHelper.loadJvmSystemPropertiesAsProperties(new
String[] { "camel.main." });
if (!propJVM.isEmpty()) {
- prop.putAll("SYS", propJVM);
+ putAllOverride(prop, "SYS", propJVM);
LOG.debug("Properties from JVM system properties:");
for (String key : propJVM.stringPropertyNames()) {
LOG.debug(" {}={}", key, propJVM.getProperty(key));
}
}
}
- // SYS and ENV take priority (ENV are in lower-case keys)
- String loc = "ENV";
- Object value = propENV != null ? propENV.remove(lowerOptionName) :
null;
- if (ObjectHelper.isNotEmpty(propJVM)) {
- Object val = propJVM.remove(optionName);
- if (ObjectHelper.isNotEmpty(val)) {
- // SYS override ENV
- loc = "SYS";
- value = val;
- }
- }
- if (ObjectHelper.isEmpty(value)) {
- // then try properties
- loc = prop.getLocation(optionName);
- value = prop.remove(optionName);
+ // SYS and ENV have been merged with priority (the key can be in any
case such as from ENV)
+ String loc = null;
+ Object value = null;
+ String key = findKeyIgnoreCase(prop, optionName);
+ if (key != null) {
+ loc = prop.getLocation(key);
+ value = prop.remove(key);
}
if (ObjectHelper.isEmpty(value)) {
// fallback to initial properties
@@ -1273,7 +1305,7 @@ public abstract class BaseMainSupport extends BaseService
{
Properties propENV
=
MainHelper.loadEnvironmentVariablesAsProperties(GROUP_PREFIXES);
if (!propENV.isEmpty()) {
- prop.putAll("ENV", propENV);
+ putAllOverride(prop, "ENV", propENV);
LOG.debug("Properties from OS environment variables:");
for (String key : propENV.stringPropertyNames()) {
LOG.debug(" {}={}", key, propENV.getProperty(key));
@@ -1284,7 +1316,7 @@ public abstract class BaseMainSupport extends BaseService
{
if
(mainConfigurationProperties.isAutoConfigurationSystemPropertiesEnabled()) {
Properties propJVM =
MainHelper.loadJvmSystemPropertiesAsProperties(GROUP_PREFIXES);
if (!propJVM.isEmpty()) {
- prop.putAll("SYS", propJVM);
+ putAllOverride(prop, "SYS", propJVM);
LOG.debug("Properties from JVM system properties:");
for (String key : propJVM.stringPropertyNames()) {
LOG.debug(" {}={}", key, propJVM.getProperty(key));
@@ -1379,7 +1411,7 @@ public abstract class BaseMainSupport extends BaseService
{
Properties propENV
=
MainHelper.loadEnvironmentVariablesAsProperties(GROUP_PREFIXES);
if (!propENV.isEmpty()) {
- prop.putAll("ENV", propENV);
+ putAllOverride(prop, "ENV", propENV);
LOG.debug("Properties from OS environment variables:");
for (String key : propENV.stringPropertyNames()) {
LOG.debug(" {}={}", key, propENV.getProperty(key));
@@ -1390,7 +1422,7 @@ public abstract class BaseMainSupport extends BaseService
{
if
(mainConfigurationProperties.isAutoConfigurationSystemPropertiesEnabled()) {
Properties propJVM =
MainHelper.loadJvmSystemPropertiesAsProperties(GROUP_PREFIXES);
if (!propJVM.isEmpty()) {
- prop.putAll("SYS", propJVM);
+ putAllOverride(prop, "SYS", propJVM);
LOG.debug("Properties from JVM system properties:");
for (String key : propJVM.stringPropertyNames()) {
LOG.debug(" {}={}", key, propJVM.getProperty(key));
@@ -1714,12 +1746,6 @@ public abstract class BaseMainSupport extends
BaseService {
mainConfigurationProperties.isAutoConfigurationFailFast(),
autoConfiguredProperties);
}
- if (!sslProperties.isEmpty() ||
mainConfigurationProperties.hasSslConfiguration()) {
- LOG.debug("Auto-configuring SSL from loaded properties: {}",
sslProperties.size());
- setSslProperties(camelContext, sslProperties,
- mainConfigurationProperties.isAutoConfigurationFailFast(),
- autoConfiguredProperties);
- }
if (!debuggerProperties.isEmpty() ||
mainConfigurationProperties.hasDebuggerConfiguration()) {
LOG.debug("Auto-configuring Debugger from loaded properties: {}",
debuggerProperties.size());
setDebuggerProperties(camelContext, debuggerProperties,
@@ -2810,14 +2836,14 @@ public abstract class BaseMainSupport extends
BaseService {
Properties propENV
= MainHelper.loadEnvironmentVariablesAsProperties(new
String[] { "camel.component.properties." });
if (!propENV.isEmpty()) {
- prop.putAll("ENV", propENV);
+ putAllOverride(prop, "ENV", propENV);
}
}
// load properties from JVM (override existing)
if
(mainConfigurationProperties.isAutoConfigurationSystemPropertiesEnabled()) {
Properties propJVM =
MainHelper.loadJvmSystemPropertiesAsProperties(new String[] {
"camel.component.properties." });
if (!propJVM.isEmpty()) {
- prop.putAll("SYS", propJVM);
+ putAllOverride(prop, "SYS", propJVM);
}
}
@@ -2866,7 +2892,7 @@ public abstract class BaseMainSupport extends BaseService
{
propENV.remove(MainConstants.OVERRIDE_PROPERTIES_LOCATION.replace('-', '.'));
propENV.remove(MainConstants.PROPERTY_PLACEHOLDER_LOCATION.replace('-', '.'));
if (!propENV.isEmpty()) {
- prop.putAll("ENV", propENV);
+ putAllOverride(prop, "ENV", propENV);
}
}
// load properties from JVM (override existing)
@@ -2880,7 +2906,7 @@ public abstract class BaseMainSupport extends BaseService
{
propJVM.remove(MainConstants.PROPERTY_PLACEHOLDER_LOCATION);
propJVM.remove(StringHelper.dashToCamelCase(MainConstants.PROPERTY_PLACEHOLDER_LOCATION));
if (!propJVM.isEmpty()) {
- prop.putAll("SYS", propJVM);
+ putAllOverride(prop, "SYS", propJVM);
}
}
@@ -2942,7 +2968,7 @@ public abstract class BaseMainSupport extends BaseService
{
}
if (!propENV.isEmpty()) {
- prop.putAll("ENV", propENV);
+ putAllOverride(prop, "ENV", propENV);
}
}
// load properties from JVM (override existing)
@@ -2950,7 +2976,7 @@ public abstract class BaseMainSupport extends BaseService
{
Properties propJVM =
MainHelper.loadJvmSystemPropertiesAsProperties(
new String[] { "camel.component.", "camel.dataformat.",
"camel.language." });
if (!propJVM.isEmpty()) {
- prop.putAll("SYS", propJVM);
+ putAllOverride(prop, "SYS", propJVM);
}
}
diff --git
a/core/camel-main/src/main/java/org/apache/camel/main/MainCommandLineSupport.java
b/core/camel-main/src/main/java/org/apache/camel/main/MainCommandLineSupport.java
index 104dc0d17040..f6e69f296d1e 100644
---
a/core/camel-main/src/main/java/org/apache/camel/main/MainCommandLineSupport.java
+++
b/core/camel-main/src/main/java/org/apache/camel/main/MainCommandLineSupport.java
@@ -242,13 +242,8 @@ public abstract class MainCommandLineSupport extends
MainSupport {
@Override
protected void configurePropertiesService(CamelContext camelContext)
throws Exception {
- if (mainConfigurationProperties.getProfile() != null) {
- // setup property placeholder location to include the profile
based properties file also
- defaultPropertyPlaceholderLocation
- =
String.format("classpath:application-%s.properties;optional=true," +
defaultPropertyPlaceholderLocation,
- mainConfigurationProperties.getProfile());
- }
-
+ // the profile based properties file is added by the base class, where
the profile from SYS or ENV takes
+ // precedence over the configured profile
super.configurePropertiesService(camelContext);
PropertiesComponent pc = camelContext.getPropertiesComponent();
diff --git
a/core/camel-main/src/main/java/org/apache/camel/main/MainDurationEventNotifier.java
b/core/camel-main/src/main/java/org/apache/camel/main/MainDurationEventNotifier.java
index c94ed37a44a6..9d60fd720254 100644
---
a/core/camel-main/src/main/java/org/apache/camel/main/MainDurationEventNotifier.java
+++
b/core/camel-main/src/main/java/org/apache/camel/main/MainDurationEventNotifier.java
@@ -16,7 +16,6 @@
*/
package org.apache.camel.main;
-import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
@@ -42,11 +41,11 @@ public class MainDurationEventNotifier extends
EventNotifierSupport implements N
STOP;
static Action toAction(String action) {
- if ("shutdown".equals(action)) {
+ if ("shutdown".equalsIgnoreCase(action)) {
return SHUTDOWN;
}
- if ("stop".equals(action)) {
+ if ("stop".equalsIgnoreCase(action)) {
return STOP;
}
@@ -61,10 +60,13 @@ public class MainDurationEventNotifier extends
EventNotifierSupport implements N
private final MainShutdownStrategy shutdownStrategy;
private final boolean stopCamelContext;
private final boolean restartDuration;
+ private final String actionName;
private final Action action;
private final LongAdder doneMessages;
private volatile StopWatch watch;
private volatile ScheduledExecutorService idleExecutorService;
+ // whether the idle event has been triggered (and not again until there is
new activity)
+ private volatile boolean idleTriggered;
public MainDurationEventNotifier(CamelContext camelContext, int
maxMessages, long maxIdleSeconds,
MainShutdownStrategy shutdownStrategy,
boolean stopCamelContext,
@@ -75,6 +77,7 @@ public class MainDurationEventNotifier extends
EventNotifierSupport implements N
this.shutdownStrategy = shutdownStrategy;
this.stopCamelContext = stopCamelContext;
this.restartDuration = restartDuration;
+ this.actionName = action;
this.action = Action.toAction(action);
this.doneMessages = new LongAdder();
@@ -152,6 +155,7 @@ public class MainDurationEventNotifier extends
EventNotifierSupport implements N
LOG.trace("Message activity so restarting stop watch");
watch.restart();
}
+ idleTriggered = false;
}
}
@@ -163,6 +167,7 @@ public class MainDurationEventNotifier extends
EventNotifierSupport implements N
if (watch != null) {
watch.restart();
}
+ idleTriggered = false;
}
}
@@ -182,7 +187,7 @@ public class MainDurationEventNotifier extends
EventNotifierSupport implements N
super.doInit();
if (action == null) {
- throw new IllegalArgumentException("Unknown action: " + action);
+ throw new IllegalArgumentException("Unknown action: " +
actionName);
}
}
@@ -193,11 +198,20 @@ public class MainDurationEventNotifier extends
EventNotifierSupport implements N
camelContext.addStartupListener((context, alreadyStarted) -> watch
= new StopWatch());
// okay we need to trigger on idle after X period, and therefore
we need a background task that checks this
- idleExecutorService = Executors.newSingleThreadScheduledExecutor();
+ idleExecutorService = camelContext.getExecutorServiceManager()
+ .newSingleThreadScheduledExecutor(this,
"CamelMainDurationIdle");
idleExecutorService.scheduleAtFixedRate(this::idleTask, 1, 1,
TimeUnit.SECONDS);
}
}
+ @Override
+ protected void doStop() throws Exception {
+ if (idleExecutorService != null) {
+
camelContext.getExecutorServiceManager().shutdownNow(idleExecutorService);
+ idleExecutorService = null;
+ }
+ }
+
private void stopTask() {
// don't run the task if shutdown is in process
if (!shutdownStrategy.isRunAllowed()) {
@@ -218,8 +232,9 @@ public class MainDurationEventNotifier extends
EventNotifierSupport implements N
}
// shutdown idle checker if in use as we are stopping
- if (idleExecutorService != null) {
- idleExecutorService.shutdownNow();
+ ScheduledExecutorService ses = idleExecutorService;
+ if (ses != null) {
+ ses.shutdownNow();
}
try {
@@ -257,7 +272,9 @@ public class MainDurationEventNotifier extends
EventNotifierSupport implements N
boolean result = seconds >= maxIdleSeconds;
LOG.trace("Duration max idle check {} >= {} -> {}", seconds,
maxIdleSeconds, result);
- if (result && shutdownStrategy.isRunAllowed()) {
+ if (result && !idleTriggered && shutdownStrategy.isRunAllowed()) {
+ // only trigger once (until there is new activity)
+ idleTriggered = true;
triggerIdleEvent();
}
}
diff --git
a/core/camel-main/src/main/java/org/apache/camel/main/MainSupport.java
b/core/camel-main/src/main/java/org/apache/camel/main/MainSupport.java
index a9b8aea72a96..9f2d109a0fa9 100644
--- a/core/camel-main/src/main/java/org/apache/camel/main/MainSupport.java
+++ b/core/camel-main/src/main/java/org/apache/camel/main/MainSupport.java
@@ -280,13 +280,15 @@ public abstract class MainSupport extends BaseMainSupport
{
}
protected void waitUntilCompleted() {
+ // whether the duration max seconds has stopped all routes (and then
wait until Camel is shutdown)
+ boolean routesStopped = false;
while (shutdownStrategy.isRunAllowed()) {
try {
int idle = durationMaxIdleSeconds;
int max = durationMaxMessages;
long sec = durationMaxSeconds;
int exit = durationHitExitCode;
- if (sec == -1 || sec > 0) {
+ if (!routesStopped && (sec == -1 || sec > 0)) {
boolean zero;
if (sec > 0) {
LOG.info("Waiting until complete: Duration max {}
seconds", sec);
@@ -303,7 +305,8 @@ public abstract class MainSupport extends BaseMainSupport {
} catch (Exception e) {
LOG.warn("Error during stopping all routes.
This exception is ignored.", e);
}
- // we are just stopping routes (not terminating
JVM) so continue
+ // we are just stopping routes (not terminating
JVM) so continue and wait until shutdown
+ routesStopped = true;
continue;
}
}
diff --git
a/core/camel-main/src/main/java/org/apache/camel/main/ProfileConfigurer.java
b/core/camel-main/src/main/java/org/apache/camel/main/ProfileConfigurer.java
index 209d7595328d..d77fbf85e8ed 100644
--- a/core/camel-main/src/main/java/org/apache/camel/main/ProfileConfigurer.java
+++ b/core/camel-main/src/main/java/org/apache/camel/main/ProfileConfigurer.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.main;
+import java.util.Locale;
import java.util.Properties;
import org.apache.camel.CamelContext;
@@ -134,9 +135,28 @@ public class ProfileConfigurer {
}
private static void setIfNotConfigured(Properties autoConfigured, String
key, Runnable setter) {
- if (autoConfigured == null || !autoConfigured.containsKey(key)) {
+ if (autoConfigured == null || !isConfigured(autoConfigured, key)) {
setter.run();
}
}
+ private static boolean isConfigured(Properties autoConfigured, String key)
{
+ if (autoConfigured.containsKey(key)) {
+ return true;
+ }
+ // the key may be in another case or with dashes (such as
camel.main.shutdowntimeout from the ENV variable
+ // CAMEL_MAIN_SHUTDOWNTIMEOUT or camel.main.shutdown-timeout)
+ String target = normalizeKey(key);
+ for (String name : autoConfigured.stringPropertyNames()) {
+ if (normalizeKey(name).equals(target)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String normalizeKey(String key) {
+ return key.toLowerCase(Locale.ENGLISH).replace("-", "").replace("_",
"");
+ }
+
}
diff --git
a/core/camel-main/src/test/java/org/apache/camel/main/MainBootstrapEdgeCasesTest.java
b/core/camel-main/src/test/java/org/apache/camel/main/MainBootstrapEdgeCasesTest.java
new file mode 100644
index 000000000000..35f8c5dc27b2
--- /dev/null
+++
b/core/camel-main/src/test/java/org/apache/camel/main/MainBootstrapEdgeCasesTest.java
@@ -0,0 +1,248 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.camel.main;
+
+import java.lang.reflect.Field;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.impl.engine.DefaultRouteController;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.LoggerConfig;
+import org.apache.logging.log4j.core.config.Property;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.parallel.Resources;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+// uses JVM system properties and captures logging, so it must not run in
parallel with other tests
+@Isolated
+@ResourceLock(Resources.SYSTEM_PROPERTIES)
+public class MainBootstrapEdgeCasesTest {
+
+ private static class CountingRouteController extends
DefaultRouteController {
+ private final AtomicInteger stopAll = new AtomicInteger();
+
+ @Override
+ public void stopAllRoutes() throws Exception {
+ stopAll.incrementAndGet();
+ super.stopAllRoutes();
+ }
+ }
+
+ private static class MyRoute extends RouteBuilder {
+ @Override
+ public void configure() {
+ from("timer:foo?period=60000").routeId("foo").to("log:foo");
+ }
+ }
+
+ @Test
+ public void testDevProfileKeepsOptionInLowerCase() {
+ Main main = new Main();
+ main.configure().withProfile("dev");
+ // the ENV variable CAMEL_MAIN_SHUTDOWNTIMEOUT is in lower case
+ main.addInitialProperty("camel.main.shutdowntimeout", "5");
+ main.addInitialProperty("camel.main.message-history", "false");
+ main.start();
+ try {
+ assertEquals(5,
main.getCamelContext().getShutdownStrategy().getTimeout());
+ assertFalse(main.getCamelContext().isMessageHistory());
+ } finally {
+ main.stop();
+ }
+ }
+
+ @Test
+ public void testSystemPropertyOverridesLowerCaseOption() {
+ Main main = new Main();
+ main.addInitialProperty("camel.main.shutdowntimeout", "11");
+ System.setProperty("camel.main.shutdownTimeout", "22");
+ try {
+ main.start();
+ assertEquals(22,
main.getCamelContext().getShutdownStrategy().getTimeout());
+ } finally {
+ System.clearProperty("camel.main.shutdownTimeout");
+ main.stop();
+ }
+ }
+
+ @Test
+ public void testSystemPropertyProfileWins() {
+ Main main = new Main();
+ main.configure().withProfile("dev");
+ System.setProperty("camel.main.profile", "prod");
+ try {
+ main.start();
+ List<String> locations =
main.getCamelContext().getPropertiesComponent().getLocations();
+ assertTrue(locations.stream().anyMatch(l ->
l.contains("application-prod")), locations.toString());
+ assertFalse(locations.stream().anyMatch(l ->
l.contains("application-dev")), locations.toString());
+ } finally {
+ System.clearProperty("camel.main.profile");
+ main.stop();
+ }
+ }
+
+ @Test
+ public void testSslConfiguredOnce() {
+ List<String> messages = new CopyOnWriteArrayList<>();
+ String loggerName = BaseMainSupport.class.getName();
+ LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
+ Configuration config = ctx.getConfiguration();
+ AbstractAppender appender = new
AbstractAppender("MainBootstrapEdgeCasesTest", null, null, true,
Property.EMPTY_ARRAY) {
+ @Override
+ public void append(LogEvent event) {
+ messages.add(event.getMessage().getFormattedMessage());
+ }
+ };
+ appender.start();
+ LoggerConfig loggerConfig = new LoggerConfig(loggerName, Level.INFO,
true);
+ loggerConfig.addAppender(appender, Level.INFO, null);
+ config.addLogger(loggerName, loggerConfig);
+ ctx.updateLoggers();
+
+ Main main = new Main();
+ main.addInitialProperty("camel.ssl.enabled", "true");
+ main.addInitialProperty("camel.ssl.selfSigned", "true");
+ try {
+ main.start();
+ long count = messages.stream().filter(m ->
m.startsWith("Generating self-signed SSL certificate")).count();
+ assertEquals(1, count, messages.toString());
+ } finally {
+ main.stop();
+ config.removeLogger(loggerName);
+ ctx.updateLoggers();
+ appender.stop();
+ }
+ }
+
+ @Test
+ public void testModelineUsesExcludePatternFromProperties() {
+ AtomicReference<String> exclude = new AtomicReference<>();
+ Main main = new Main() {
+ @Override
+ protected void modelineRoutes(CamelContext camelContext) throws
Exception {
+
exclude.set(mainConfigurationProperties.getRoutesExcludePattern());
+ super.modelineRoutes(camelContext);
+ }
+ };
+ main.addInitialProperty("camel.main.routesExcludePattern",
"**/broken.yaml");
+ main.start();
+ try {
+ assertEquals("**/broken.yaml", exclude.get());
+ } finally {
+ main.stop();
+ }
+ }
+
+ @Test
+ public void testDurationMaxSecondsMinusOneWithStopAction() throws
Exception {
+ CountingRouteController rc = new CountingRouteController();
+ Main main = new Main() {
+ @Override
+ protected CamelContext createCamelContext() {
+ CamelContext answer = super.createCamelContext();
+ answer.setRouteController(rc);
+ return answer;
+ }
+ };
+ main.configure().addRoutesBuilder(new MyRoute());
+ main.configure().withDurationMaxSeconds(-1);
+ main.configure().withDurationMaxAction("stop");
+
+ Thread t = new Thread(() -> {
+ try {
+ main.run();
+ } catch (Exception e) {
+ // ignore
+ }
+ });
+ t.start();
+ try {
+ await().atMost(Duration.ofSeconds(20)).until(() ->
rc.stopAll.get() >= 1);
+ // stopping the routes is done once (and not in a busy loop)
+
await().during(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(5)).until(() ->
rc.stopAll.get() == 1);
+ } finally {
+ main.completed();
+ t.join(20000);
+ }
+ }
+
+ @Test
+ public void testDurationEventNotifierStopsIdleExecutor() throws Exception {
+ CamelContext context = new DefaultCamelContext();
+ context.start();
+ try {
+ MainDurationEventNotifier notifier = new MainDurationEventNotifier(
+ context, 0, 5, new SimpleMainShutdownStrategy(), false,
false, "shutdown");
+ notifier.start();
+ Field field =
MainDurationEventNotifier.class.getDeclaredField("idleExecutorService");
+ field.setAccessible(true);
+ ScheduledExecutorService executor = (ScheduledExecutorService)
field.get(notifier);
+ assertFalse(executor.isShutdown());
+ notifier.stop();
+ assertTrue(executor.isShutdown());
+ } finally {
+ context.stop();
+ }
+ }
+
+ @Test
+ public void testDurationIdleWithStopActionTriggersOnce() throws Exception {
+ CountingRouteController rc = new CountingRouteController();
+ CamelContext context = new DefaultCamelContext();
+ context.setRouteController(rc);
+ MainDurationEventNotifier notifier = new MainDurationEventNotifier(
+ context, 0, 1, new SimpleMainShutdownStrategy(), false, false,
"stop");
+ notifier.start();
+ context.start();
+ try {
+ await().atMost(Duration.ofSeconds(20)).until(() ->
rc.stopAll.get() >= 1);
+
await().during(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(6)).until(() ->
rc.stopAll.get() == 1);
+ } finally {
+ notifier.stop();
+ context.stop();
+ }
+ }
+
+ @Test
+ public void testDurationActionIgnoresCase() throws Exception {
+ CamelContext context = new DefaultCamelContext();
+ MainDurationEventNotifier notifier = new MainDurationEventNotifier(
+ context, 10, 0, new SimpleMainShutdownStrategy(), false,
false, "STOP");
+ assertDoesNotThrow(notifier::start);
+ notifier.stop();
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index c8dfc1d901c9..7b761b7d5696 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -2926,6 +2926,23 @@ To restore the previous behaviour and allow external
entity resolution, set the
`allowExternalEntities` option to `true` on the data format or on the endpoint
(`smooks:config.xml?allowExternalEntities=true`).
+=== camel-main - profiles and precedence of ENV and JVM system properties
+
+The `dev` profile no longer overrides an option that is configured with a key
in another case or with dashes, such as
+the ENV variable `CAMEL_MAIN_SHUTDOWNTIMEOUT=5` (which becomes
`camel.main.shutdowntimeout`) or
+`camel.main.shutdown-timeout=5`. Previously only the exact camelCase key
(`camel.main.shutdownTimeout`) was
+recognized, and the `dev` profile default was used instead.
+
+A JVM system property now always takes precedence over the same option from an
ENV variable or from a properties
+file, also when the keys are in another case (such as
`-Dcamel.main.shutdownTimeout=22` and
+`CAMEL_MAIN_SHUTDOWNTIMEOUT=11`). Previously the result depended on the case
of the keys.
+
+When the profile is set as an ENV variable or JVM system property
(`camel.main.profile`), only the
+`application-<profile>.properties` file of that profile is loaded. Previously
`Main` also loaded the properties file of
+a profile configured in Java (such as `main.configure().withProfile("dev")`).
+
+The option `camel.main.durationMaxAction` is now case-insensitive (`STOP` is
the same as `stop`).
+
=== camel-zipfile, camel-tarfile - a maxDecompressedSize of 2 GiB or more is
now enforced
The `maxDecompressedSize` option of the Zip File and Tar File data formats
(and of `ZipSplitter`) was not