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,