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 6c8c962ff130 CAMEL-25111: camel-core - Properties component: fix bugs 
found in a deep review (#27017)
6c8c962ff130 is described below

commit 6c8c962ff1309876388d022fb99a9d8f26f110f0
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 29 00:27:29 2026 +0200

    CAMEL-25111: camel-core - Properties component: fix bugs found in a deep 
review (#27017)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../AbstractLocationPropertiesSource.java          |  18 +-
 .../DefaultPropertiesFunctionResolver.java         |  16 +-
 .../properties/DefaultPropertiesLookup.java        |   7 +-
 .../DefaultPropertiesLookupListener.java           |   3 +-
 .../component/properties/PropertiesComponent.java  |  29 ++-
 .../component/properties/PropertiesLocation.java   |   3 +-
 .../properties/ServicePropertiesFunction.java      |   6 +-
 .../PropertiesComponentEdgeCasesTest.java          | 285 +++++++++++++++++++++
 .../org/apache/camel/properties-function/edgecount |  17 ++
 .../main/java/org/apache/camel/util/IOHelper.java  |   5 +-
 .../java/org/apache/camel/util/IOHelperTest.java   |  13 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  10 +
 12 files changed, 387 insertions(+), 25 deletions(-)

diff --git 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/AbstractLocationPropertiesSource.java
 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/AbstractLocationPropertiesSource.java
index ee735a3cc1ac..d77085d107a7 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/AbstractLocationPropertiesSource.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/AbstractLocationPropertiesSource.java
@@ -32,7 +32,8 @@ import org.apache.camel.util.OrderedProperties;
 public abstract class AbstractLocationPropertiesSource extends ServiceSupport
         implements LoadablePropertiesSource, LocationPropertiesSource {
 
-    private final Properties properties = new OrderedProperties();
+    // volatile as the properties are replaced when reloading
+    private volatile Properties properties = new OrderedProperties();
     private final PropertiesComponent propertiesComponent;
     private final PropertiesLocation location;
 
@@ -57,9 +58,10 @@ public abstract class AbstractLocationPropertiesSource 
extends ServiceSupport
     public Properties loadProperties(Predicate<String> filter) {
         Properties answer = new OrderedProperties();
 
-        for (String name : properties.stringPropertyNames()) {
+        Properties current = properties;
+        for (String name : current.stringPropertyNames()) {
             if (filter.test(name)) {
-                answer.put(name, properties.get(name));
+                answer.put(name, current.get(name));
             }
         }
 
@@ -72,13 +74,13 @@ public abstract class AbstractLocationPropertiesSource 
extends ServiceSupport
         if (resolver != null) {
             location = location.substring(resolver.length());
         }
-        PropertiesLocation loc = new PropertiesLocation(resolver, location);
+        // keep the optional flag of the location
+        PropertiesLocation loc = new PropertiesLocation(resolver, location, 
this.location.isOptional());
         Properties prop = loadPropertiesFromLocation(propertiesComponent, loc);
         if (prop != null) {
-            prop = prepareLoadedProperties(prop);
-            // need to clear in case some properties was removed
-            properties.clear();
-            properties.putAll(prop);
+            // replace the properties (in case some properties was removed) in 
one step, so concurrent lookups
+            // see either the old or the new properties
+            properties = prepareLoadedProperties(prop);
         }
     }
 
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesFunctionResolver.java
 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesFunctionResolver.java
index 280ad64f12c8..fc402d0424cc 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesFunctionResolver.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesFunctionResolver.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.component.properties;
 
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
@@ -40,7 +41,8 @@ public class DefaultPropertiesFunctionResolver extends 
ServiceSupport
 
     private CamelContext camelContext;
     private FactoryFinder factoryFinder;
-    private final Map<String, PropertiesFunction> functions = new 
LinkedHashMap<>();
+    // thread-safe as functions may be resolved at runtime from concurrent 
threads
+    private final Map<String, PropertiesFunction> functions = 
Collections.synchronizedMap(new LinkedHashMap<>());
 
     public DefaultPropertiesFunctionResolver() {
     }
@@ -74,9 +76,15 @@ public class DefaultPropertiesFunctionResolver extends 
ServiceSupport
     public PropertiesFunction resolvePropertiesFunction(String name) {
         PropertiesFunction answer = functions.get(name);
         if (answer == null) {
-            answer = resolve(camelContext, name);
-            if (answer != null) {
-                functions.put(name, answer);
+            // resolve (and start) the function only once
+            synchronized (functions) {
+                answer = functions.get(name);
+                if (answer == null) {
+                    answer = resolve(camelContext, name);
+                    if (answer != null) {
+                        functions.put(name, answer);
+                    }
+                }
             }
         }
         return answer;
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesLookup.java
 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesLookup.java
index 695edcee6405..61578449bb00 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesLookup.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesLookup.java
@@ -81,7 +81,7 @@ public class DefaultPropertiesLookup implements 
PropertiesLookup {
             Object value = component.getOverrideProperties().get(name);
             if (value != null) {
                 answer = 
component.getCamelContext().getTypeConverter().mandatoryConvertTo(String.class, 
value);
-                String loc = location(local, name, "OverrideProperties");
+                String loc = location(component.getOverrideProperties(), name, 
"OverrideProperties");
                 onLookup(name, answer, defaultValue, loc);
             }
         }
@@ -100,7 +100,8 @@ public class DefaultPropertiesLookup implements 
PropertiesLookup {
                     } else if (ps instanceof LocationPropertiesSource 
propSource) {
                         source = propSource.getLocation().getPath();
                     } else if (ps instanceof LoadablePropertiesSource 
propSource) {
-                        Properties prop = propSource.loadProperties();
+                        // only load this property (loading all the properties 
can be expensive for some sources)
+                        Properties prop = 
propSource.loadProperties(name::equals);
                         if (prop instanceof OrderedLocationProperties olp) {
                             source = olp.getLocation(name);
                         }
@@ -116,7 +117,7 @@ public class DefaultPropertiesLookup implements 
PropertiesLookup {
             Object value = component.getInitialProperties().get(name);
             if (value != null) {
                 answer = 
component.getCamelContext().getTypeConverter().mandatoryConvertTo(String.class, 
value);
-                String loc = location(local, name, "InitialProperties");
+                String loc = location(component.getInitialProperties(), name, 
"InitialProperties");
                 onLookup(name, answer, defaultValue, loc);
             }
         }
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesLookupListener.java
 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesLookupListener.java
index d9af8fad85fe..3a5172a7e871 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesLookupListener.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesLookupListener.java
@@ -17,6 +17,7 @@
 package org.apache.camel.component.properties;
 
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.camel.PropertiesLookupListener;
 import org.apache.camel.spi.PropertiesResolvedValue;
@@ -29,7 +30,7 @@ import org.apache.camel.support.service.ServiceSupport;
  */
 public class DefaultPropertiesLookupListener extends ServiceSupport implements 
PropertiesLookupListener {
 
-    private Map<String, PropertiesResolvedValue> properties;
+    private Map<String, PropertiesResolvedValue> properties = new 
ConcurrentHashMap<>();
 
     @Override
     public void onLookup(String name, String value, String defaultValue, 
String source) {
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/PropertiesComponent.java
 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/PropertiesComponent.java
index 7afb4c9fb2f8..0a91a401e08a 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/PropertiesComponent.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/PropertiesComponent.java
@@ -24,6 +24,7 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Properties;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -114,7 +115,9 @@ public class PropertiesComponent extends ServiceSupport
     private final List<PropertiesLookupListener> propertiesLookupListeners = 
new ArrayList<>();
     private final PropertiesSourceFactory propertiesSourceFactory = new 
DefaultPropertiesSourceFactory(this);
     private final DefaultPropertiesLookupListener 
defaultPropertiesLookupListener = new DefaultPropertiesLookupListener();
-    private final List<PropertiesSource> sources = new ArrayList<>();
+    private final List<PropertiesLocation> unknownLocations = new 
ArrayList<>();
+    // thread-safe as sources may be added at runtime (such as when reloading) 
while properties are looked up
+    private final List<PropertiesSource> sources = new 
CopyOnWriteArrayList<>();
     private List<PropertiesLocation> locations = new ArrayList<>();
     private String location;
     private boolean ignoreMissingLocation;
@@ -372,6 +375,7 @@ public class PropertiesComponent extends ServiceSupport
 
         // we need to re-create the property sources which may have already 
been created from locations
         this.sources.removeIf(s -> s instanceof LocationPropertiesSource);
+        this.unknownLocations.clear();
         // ensure the locations are in the same order as here, and therefore 
we provide the order number
         int order = 100;
         for (PropertiesLocation loc : locations) {
@@ -400,7 +404,11 @@ public class PropertiesComponent extends ServiceSupport
     }
 
     public void addLocation(PropertiesLocation location) {
-        this.locations.add(location);
+        if (location != null) {
+            List<PropertiesLocation> newLocations = new ArrayList<>(locations);
+            newLocations.add(location);
+            setLocations(newLocations);
+        }
     }
 
     @Override
@@ -808,6 +816,7 @@ public class PropertiesComponent extends ServiceSupport
 
     @Override
     protected void doStart() throws Exception {
+        checkUnknownLocations();
         ServiceHelper.startService(sources, propertiesFunctionResolver, 
defaultPropertiesLookupListener);
     }
 
@@ -828,6 +837,22 @@ public class PropertiesComponent extends ServiceSupport
             addPropertiesSource(new FilePropertiesSource(this, location, 
order));
         } else if ("classpath".equals(location.getResolver())) {
             addPropertiesSource(new ClasspathPropertiesSource(this, location, 
order));
+        } else if (!location.isOptional()) {
+            // validated when starting (as ignoreMissingLocation may be 
configured afterwards)
+            unknownLocations.add(location);
+            if (isStarted()) {
+                checkUnknownLocations();
+            }
+        } else {
+            LOG.debug("Ignored properties location with unknown resolver: {}", 
location);
+        }
+    }
+
+    private void checkUnknownLocations() {
+        if (!ignoreMissingLocation && !unknownLocations.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "Unknown resolver in properties locations: " + 
unknownLocations
+                                               + ". Supported resolvers are: 
classpath, file, ref.");
         }
     }
 
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/PropertiesLocation.java
 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/PropertiesLocation.java
index eac27070e078..f440cb678d3e 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/PropertiesLocation.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/PropertiesLocation.java
@@ -38,7 +38,8 @@ public final class PropertiesLocation {
         }
 
         idx = location.lastIndexOf(';');
-        if (idx != -1) {
+        // only when the location ends with an option (a path may contain a 
semicolon)
+        if (idx != -1 && location.substring(idx + 1).startsWith("optional=")) {
             this.optional = StringHelper.after(location.substring(idx + 1), 
"optional=", Boolean::valueOf).orElse(false);
             location = location.substring(0, idx);
         } else {
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/ServicePropertiesFunction.java
 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/ServicePropertiesFunction.java
index 4c59acbdc11c..348b5d01d7b4 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/ServicePropertiesFunction.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/ServicePropertiesFunction.java
@@ -16,8 +16,6 @@
  */
 package org.apache.camel.component.properties;
 
-import java.util.Locale;
-
 import org.apache.camel.spi.PropertiesFunction;
 import org.apache.camel.util.IOHelper;
 import org.apache.camel.util.StringHelper;
@@ -54,8 +52,8 @@ public class ServicePropertiesFunction implements 
PropertiesFunction {
 
         // make sure to use upper case
         if (key != null) {
-            // make sure to use underscore as dash is not supported as ENV 
variables
-            key = key.toUpperCase(Locale.ENGLISH).replace('-', '_');
+            // the environment variable name is normalized when looking up (as 
for the service.host and service.port
+            // functions), so camelCase and dash is supported
 
             // a service should have both the host and port defined
             String host = IOHelper.lookupEnvironmentVariable(key + 
HOST_PREFIX);
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesComponentEdgeCasesTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesComponentEdgeCasesTest.java
new file mode 100644
index 000000000000..d34456367948
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesComponentEdgeCasesTest.java
@@ -0,0 +1,285 @@
+/*
+ * 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.component.properties;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.LoadablePropertiesSource;
+import org.apache.camel.spi.PropertiesFunction;
+import org.apache.camel.spi.PropertiesResolvedValue;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.OrderedLocationProperties;
+import org.apache.camel.util.OrderedProperties;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class PropertiesComponentEdgeCasesTest {
+
+    static final AtomicInteger CREATED = new AtomicInteger();
+
+    @TempDir
+    Path dir;
+
+    public static class CountingFunction extends ServiceSupport implements 
PropertiesFunction {
+        public CountingFunction() {
+            CREATED.incrementAndGet();
+        }
+
+        @Override
+        public String getName() {
+            return "edgecount";
+        }
+
+        @Override
+        public String apply(String remainder) {
+            return remainder;
+        }
+    }
+
+    private static class MySource implements LoadablePropertiesSource {
+        private final AtomicInteger loadAll = new AtomicInteger();
+
+        @Override
+        public String getName() {
+            return "mySource";
+        }
+
+        @Override
+        public String getProperty(String name) {
+            return "foo".equals(name) ? "bar" : null;
+        }
+
+        @Override
+        public Properties loadProperties() {
+            loadAll.incrementAndGet();
+            Properties answer = new OrderedProperties();
+            answer.put("foo", "bar");
+            return answer;
+        }
+
+        @Override
+        public Properties loadProperties(Predicate<String> filter) {
+            Properties answer = new OrderedProperties();
+            if (filter.test("foo")) {
+                answer.put("foo", "bar");
+            }
+            return answer;
+        }
+
+        @Override
+        public void reloadProperties(String location) {
+            // noop
+        }
+    }
+
+    @Test
+    public void testReloadIsAtomic() throws Exception {
+        Path file = dir.resolve("my.properties");
+        Files.writeString(file, "greeting=Hello\n");
+
+        CamelContext context = new DefaultCamelContext();
+        context.getPropertiesComponent().setLocation("file:" + file);
+        context.start();
+        try {
+            AtomicBoolean done = new AtomicBoolean();
+            AtomicInteger misses = new AtomicInteger();
+            Thread reader = new Thread(() -> {
+                while (!done.get()) {
+                    if 
(context.getPropertiesComponent().resolveProperty("greeting").isEmpty()) {
+                        misses.incrementAndGet();
+                    }
+                }
+            });
+            reader.start();
+            for (int i = 0; i < 2000; i++) {
+                context.getPropertiesComponent().reloadProperties("*");
+            }
+            done.set(true);
+            reader.join(TimeUnit.SECONDS.toMillis(10));
+            assertEquals(0, misses.get());
+        } finally {
+            context.stop();
+        }
+    }
+
+    @Test
+    public void testAddPropertiesSourceWhileLookup() throws Exception {
+        CamelContext context = new DefaultCamelContext();
+        context.getPropertiesComponent().addInitialProperty("greeting", 
"Hello");
+        context.start();
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        try {
+            AtomicBoolean done = new AtomicBoolean();
+            Future<?> reader = executor.submit(() -> {
+                while (!done.get()) {
+                    
context.getPropertiesComponent().resolveProperty("greeting");
+                }
+                return null;
+            });
+            for (int i = 0; i < 2000; i++) {
+                context.getPropertiesComponent().addPropertiesSource(new 
MySource());
+            }
+            done.set(true);
+            assertDoesNotThrow(() -> reader.get(10, TimeUnit.SECONDS));
+        } finally {
+            executor.shutdownNow();
+            context.stop();
+        }
+    }
+
+    @Test
+    public void testLookupDoesNotLoadAllProperties() throws Exception {
+        MySource source = new MySource();
+        CamelContext context = new DefaultCamelContext();
+        context.getPropertiesComponent().addPropertiesSource(source);
+        context.start();
+        try {
+            for (int i = 0; i < 10; i++) {
+                assertEquals("bar", 
context.resolvePropertyPlaceholders("{{foo}}"));
+            }
+            assertEquals(0, source.loadAll.get());
+        } finally {
+            context.stop();
+        }
+    }
+
+    @Test
+    public void testReloadOptionalLocation() throws Exception {
+        Path file = dir.resolve("optional.properties");
+        Files.writeString(file, "greeting=Hello\n");
+
+        CamelContext context = new DefaultCamelContext();
+        context.getPropertiesComponent().setLocation("file:" + file + 
";optional=true");
+        context.start();
+        try {
+            Files.delete(file);
+            assertDoesNotThrow(() -> 
context.getPropertiesComponent().reloadProperties("*"));
+        } finally {
+            context.stop();
+        }
+    }
+
+    @Test
+    public void testUnknownLocationResolver() {
+        CamelContext context = new DefaultCamelContext();
+        
context.getPropertiesComponent().setLocation("Classpath:does-not-exist.properties");
+        assertThrows(Exception.class, context::start);
+
+        CamelContext context2 = new DefaultCamelContext();
+        
context2.getPropertiesComponent().setLocation("Classpath:does-not-exist.properties;optional=true");
+        assertDoesNotThrow(context2::start);
+        context2.stop();
+    }
+
+    @Test
+    public void testAddLocation() throws Exception {
+        Path file = dir.resolve("added.properties");
+        Files.writeString(file, "added=yes\n");
+
+        CamelContext context = new DefaultCamelContext();
+        PropertiesComponent pc = (PropertiesComponent) 
context.getPropertiesComponent();
+        
pc.setLocation("classpath:org/apache/camel/component/properties/myproperties.properties");
+        pc.addLocation(new PropertiesLocation("file:" + file));
+        context.start();
+        try {
+            assertEquals("yes", 
context.resolvePropertyPlaceholders("{{added}}"));
+        } finally {
+            context.stop();
+        }
+    }
+
+    @Test
+    public void testResolveFunctionOnce() throws Exception {
+        CamelContext context = new DefaultCamelContext();
+        context.start();
+        CREATED.set(0);
+        ExecutorService executor = Executors.newFixedThreadPool(16);
+        try {
+            CountDownLatch latch = new CountDownLatch(1);
+            List<Future<String>> futures = new ArrayList<>();
+            for (int i = 0; i < 16; i++) {
+                futures.add(executor.submit(() -> {
+                    latch.await();
+                    return 
context.resolvePropertyPlaceholders("{{edgecount:a}}");
+                }));
+            }
+            latch.countDown();
+            for (Future<String> f : futures) {
+                assertEquals("a", f.get(10, TimeUnit.SECONDS));
+            }
+            assertEquals(1, CREATED.get());
+        } finally {
+            executor.shutdownNow();
+            context.stop();
+        }
+    }
+
+    @Test
+    public void testResolvedValueBeforeBuild() {
+        PropertiesComponent pc = new PropertiesComponent();
+        assertDoesNotThrow(() -> pc.getResolvedValue("x"));
+        assertFalse(pc.getResolvedValue("x").isPresent());
+    }
+
+    @Test
+    public void testOverridePropertiesLocation() throws Exception {
+        CamelContext context = new DefaultCamelContext();
+        PropertiesComponent pc = (PropertiesComponent) 
context.getPropertiesComponent();
+        OrderedLocationProperties override = new OrderedLocationProperties();
+        override.put("myloc", "greeting", "Hello");
+        pc.setOverrideProperties(override);
+        context.start();
+        try {
+            assertEquals("Hello", 
context.resolvePropertyPlaceholders("{{greeting}}"));
+            PropertiesResolvedValue value = 
pc.getResolvedValue("greeting").get();
+            assertEquals("myloc", value.source());
+        } finally {
+            context.stop();
+        }
+    }
+
+    @Test
+    public void testLocationWithSemicolonInPath() {
+        PropertiesLocation location = new 
PropertiesLocation("file:/a;b/c.properties");
+        assertEquals("/a;b/c.properties", location.getPath());
+        assertFalse(location.isOptional());
+
+        location = new 
PropertiesLocation("file:/a/c.properties;optional=true");
+        assertEquals("/a/c.properties", location.getPath());
+        assertTrue(location.isOptional());
+    }
+}
diff --git 
a/core/camel-core/src/test/resources/META-INF/services/org/apache/camel/properties-function/edgecount
 
b/core/camel-core/src/test/resources/META-INF/services/org/apache/camel/properties-function/edgecount
new file mode 100644
index 000000000000..800bff74e397
--- /dev/null
+++ 
b/core/camel-core/src/test/resources/META-INF/services/org/apache/camel/properties-function/edgecount
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+class=org.apache.camel.component.properties.PropertiesComponentEdgeCasesTest$CountingFunction
diff --git a/core/camel-util/src/main/java/org/apache/camel/util/IOHelper.java 
b/core/camel-util/src/main/java/org/apache/camel/util/IOHelper.java
index e8948e2ce7cb..bb9d3ded681c 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/IOHelper.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/IOHelper.java
@@ -42,6 +42,7 @@ import java.nio.charset.Charset;
 import java.nio.charset.UnsupportedCharsetException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Locale;
 import java.util.Scanner;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
@@ -699,7 +700,7 @@ public final class IOHelper {
      */
     public static String lookupEnvironmentVariable(String key) {
         // lookup OS env with upper case key
-        String upperKey = key.toUpperCase();
+        String upperKey = key.toUpperCase(Locale.ENGLISH);
         String value = System.getenv(upperKey);
 
         if (value == null) {
@@ -718,7 +719,7 @@ public final class IOHelper {
      * underscores.
      */
     public static String normalizeEnvironmentVariable(String key) {
-        String upperKey = key.toUpperCase();
+        String upperKey = key.toUpperCase(Locale.ENGLISH);
         // some OS do not support dashes in keys, so replace with underscore
         String normalizedKey = upperKey.replace('-', '_');
 
diff --git 
a/core/camel-util/src/test/java/org/apache/camel/util/IOHelperTest.java 
b/core/camel-util/src/test/java/org/apache/camel/util/IOHelperTest.java
index 87013be37ab9..5303e411ef81 100644
--- a/core/camel-util/src/test/java/org/apache/camel/util/IOHelperTest.java
+++ b/core/camel-util/src/test/java/org/apache/camel/util/IOHelperTest.java
@@ -22,6 +22,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Arrays;
+import java.util.Locale;
 
 import org.junit.jupiter.api.Test;
 
@@ -118,4 +119,16 @@ public class IOHelperTest {
             return n;
         }
     }
+
+    @Test
+    public void testNormalizeEnvironmentVariableTurkishLocale() {
+        Locale before = Locale.getDefault();
+        try {
+            Locale.setDefault(Locale.forLanguageTag("tr-TR"));
+            assertEquals("FILE_INDEX", 
IOHelper.normalizeEnvironmentVariable("file.index"));
+        } finally {
+            Locale.setDefault(before);
+        }
+    }
+
 }
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 74aacb03f550..b31a927923b0 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
@@ -3591,6 +3591,16 @@ producer were never stopped.
 With `parallelProcessing`, a recipient whose task had not started yet when the 
Recipient List completed is now
 skipped instead of being sent to afterwards. As before, recipients that had 
already started keep running.
 
+=== camel-core - properties component locations
+
+A properties location with an unknown (or misspelled) resolver, such as 
`Classpath:app.properties` or
+`C:\app.properties` (use `file:` for a file path), now fails when Camel is 
started. Previously the location was silently
+ignored. The supported resolvers are `classpath:`, `file:` and `ref:`. Mark 
the location as optional
+(`;optional=true`) or set `ignoreMissingLocation=true` to ignore it.
+
+A `;` in the path of a location (such as `file:/opt/a;b/app.properties`) is 
now kept as part of the path. Previously
+the path was cut off at the `;` (only `;optional=true` is an option of a 
location).
+
 === camel-xmlsecurity, camel-avro - data format options now carry their 
security metadata
 
 The `security` and `secret` attributes of `@Metadata` were never written to 
the generated metadata of data format,

Reply via email to