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

Reply via email to