This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new bc343963a5 Marshall module improvements
bc343963a5 is described below
commit bc343963a503a1e6cded28c76588e627baf581ae
Author: James Bognar <[email protected]>
AuthorDate: Thu Dec 11 12:00:14 2025 -0500
Marshall module improvements
---
.../apache/juneau/commons/settings/MapSource.java | 156 ++++++
.../juneau/commons/settings/ReadOnlySource.java | 116 ++++
.../juneau/commons/settings/SettingSource.java | 119 ++++
.../apache/juneau/commons/settings/Settings.java | 533 ++++++++++++++++++
.../org/apache/juneau/commons/utils/Utils.java | 39 ++
.../juneau/commons/settings/Settings_Test.java | 613 +++++++++++++++++++++
6 files changed, 1576 insertions(+)
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/MapSource.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/MapSource.java
new file mode 100644
index 0000000000..f7ed272e44
--- /dev/null
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/MapSource.java
@@ -0,0 +1,156 @@
+/*
+ * 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.juneau.commons.settings;
+
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.*;
+
+/**
+ * A writable {@link SettingSource} implementation backed by a thread-safe map.
+ *
+ * <p>
+ * This class provides a mutable source for settings that can be modified at
runtime. It's particularly useful
+ * for creating custom property sources (e.g., Spring properties,
configuration files) that can be added to
+ * {@link Settings}.
+ *
+ * <h5 class='section'>Thread Safety:</h5>
+ * <p>
+ * This class is thread-safe. The internal map is lazily initialized using
{@link AtomicReference} and
+ * {@link ConcurrentHashMap} for thread-safe operations.
+ *
+ * <h5 class='section'>Null Value Handling:</h5>
+ * <p>
+ * Setting a value to <c>null</c> stores <c>Optional.empty()</c> in the map,
which means {@link #get(String)}
+ * will return <c>Optional.empty()</c> (not <c>null</c>). This allows you to
explicitly override system properties
+ * with null values. Use {@link #unset(String)} if you want to remove a key
entirely (so {@link #get(String)} returns <c>null</c>).
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Create a source and add properties</jc>
+ * MapSource <jv>source</jv> = <jk>new</jk> MapSource();
+ * <jv>source</jv>.set(<js>"my.property"</js>, <js>"value"</js>);
+ * <jv>source</jv>.set(<js>"another.property"</js>,
<js>"another-value"</js>);
+ *
+ * <jc>// Add to Settings</jc>
+ * Settings.<jsf>get</jsf>().addSource(<jv>source</jv>);
+ *
+ * <jc>// Override a system property with null</jc>
+ * <jv>source</jv>.set(<js>"system.property"</js>, <jk>null</jk>);
+ * <jc>// get() will now return Optional.empty() for "system.property"</jc>
+ *
+ * <jc>// Remove a property entirely</jc>
+ * <jv>source</jv>.unset(<js>"my.property"</js>);
+ * <jc>// get() will now return null for "my.property"</jc>
+ * </p>
+ */
+public class MapSource implements SettingSource {
+
+ private final AtomicReference<Map<String,Optional<String>>> map = new
AtomicReference<>();
+
+ /**
+ * Returns a setting from this source.
+ *
+ * <p>
+ * Returns <c>null</c> if the key doesn't exist in the map, or the
stored value (which may be
+ * <c>Optional.empty()</c> if the value was explicitly set to
<c>null</c>).
+ *
+ * @param key The property name.
+ * @return The property value, <c>null</c> if the key doesn't exist, or
<c>Optional.empty()</c> if the key
+ * exists but has a null value.
+ */
+ @Override
+ public Optional<String> get(String key) {
+ var m = map.get();
+ if (m == null)
+ return null; // Key not in source (map doesn't exist)
+ if (! m.containsKey(key))
+ return null; // Key not in source (key doesn't exist in
map)
+ // Key exists in map - return the value (which may be
Optional.empty() if value was set to null)
+ return m.get(key);
+ }
+
+ /**
+ * Sets a setting in this source.
+ *
+ * <p>
+ * The internal map is lazily initialized on the first call to this
method. Setting a value to <c>null</c>
+ * stores <c>Optional.empty()</c> in the map, which means {@link
#get(String)} will return <c>Optional.empty()</c>
+ * (not <c>null</c>). This allows you to explicitly override system
properties with null values.
+ *
+ * @param key The property name.
+ * @param value The property value, or <c>null</c> to set an empty
override.
+ */
+ @Override
+ public void set(String key, String value) {
+ if (! canWrite())
+ return;
+ var m = map.get();
+ if (m == null) {
+ var newMap = new
ConcurrentHashMap<String,Optional<String>>();
+ m = map.compareAndSet(null, newMap) ? newMap :
map.get();
+ }
+ m.put(key, opt(value));
+ }
+
+ /**
+ * Clears all entries from this source.
+ *
+ * <p>
+ * After calling this method, all keys will be removed from the map,
and {@link #get(String)} will return
+ * <c>null</c> for all keys.
+ */
+ @Override
+ public void clear() {
+ if (! canWrite())
+ return;
+ var m = map.get();
+ if (m != null)
+ m.clear();
+ }
+
+ /**
+ * Returns <c>true</c> since this source is writable.
+ *
+ * @return <c>true</c>
+ */
+ @Override
+ public boolean canWrite() {
+ return true;
+ }
+
+ /**
+ * Removes a setting from this source.
+ *
+ * <p>
+ * After calling this method, {@link #get(String)} will return
<c>null</c> for the specified key,
+ * indicating that the key doesn't exist in this source (as opposed to
returning <c>Optional.empty()</c>,
+ * which would indicate the key exists but has a null value).
+ *
+ * @param name The property name to remove.
+ */
+ @Override
+ public void unset(String name) {
+ if (! canWrite())
+ return;
+ var m = map.get();
+ if (m != null)
+ m.remove(name);
+ }
+}
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/ReadOnlySource.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/ReadOnlySource.java
new file mode 100644
index 0000000000..25f0a576d0
--- /dev/null
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/ReadOnlySource.java
@@ -0,0 +1,116 @@
+/*
+ * 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.juneau.commons.settings;
+
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.util.*;
+import java.util.function.*;
+
+/**
+ * A read-only {@link SettingSource} implementation that delegates to a
function.
+ *
+ * <p>
+ * This class provides a read-only source for settings that delegates property
lookups to a provided function.
+ * It's particularly useful for wrapping existing property sources (e.g.,
{@link System#getProperty(String)},
+ * {@link System#getenv(String)}) as {@link SettingSource} instances.
+ *
+ * <h5 class='section'>Return Value Semantics:</h5>
+ * <ul class='spaced-list'>
+ * <li>If the function returns <c>null</c>, this source returns
<c>null</c> (key doesn't exist).
+ * <li>If the function returns a non-null value, this source returns
<c>Optional.of(value)</c>.
+ * </ul>
+ *
+ * <p>
+ * Note: This source cannot distinguish between a key that doesn't exist and a
key that exists with a null value,
+ * since the function only returns a <c>String</c>. If you need to distinguish
these cases, use {@link MapSource} instead.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Create a read-only source from System.getProperty</jc>
+ * ReadOnlySource <jv>sysProps</jv> = <jk>new</jk> ReadOnlySource(x ->
System.getProperty(x));
+ *
+ * <jc>// Create a read-only source from System.getenv</jc>
+ * ReadOnlySource <jv>envVars</jv> = <jk>new</jk> ReadOnlySource(x ->
System.getenv(x));
+ *
+ * <jc>// Add to Settings</jc>
+ * Settings.<jsf>get</jsf>().addSource(<jv>sysProps</jv>);
+ * </p>
+ */
+public class ReadOnlySource implements SettingSource {
+
+ private final Function<String,String> function;
+
+ /**
+ * Constructor.
+ *
+ * @param function The function to delegate property lookups to. Must
not be <c>null</c>.
+ */
+ public ReadOnlySource(Function<String,String> function) {
+ this.function = function;
+ }
+
+ /**
+ * Returns a setting by delegating to the function.
+ *
+ * <p>
+ * If the function returns <c>null</c>, this method returns <c>null</c>
(indicating the key doesn't exist).
+ * If the function returns a non-null value, this method returns
<c>Optional.of(value)</c>.
+ *
+ * @param name The property name.
+ * @return The property value, or <c>null</c> if the function returns
<c>null</c>.
+ */
+ @Override
+ public Optional<String> get(String name) {
+ var v = function.apply(name);
+ return v == null ? null : opt(v);
+ }
+
+ /**
+ * Returns <c>false</c> since this source is read-only.
+ *
+ * @return <c>false</c>
+ */
+ @Override
+ public boolean canWrite() {
+ return false;
+ }
+
+ /**
+ * No-op since this source is read-only.
+ *
+ * @param name The property name (ignored).
+ * @param value The property value (ignored).
+ */
+ @Override
+ public void set(String name, String value) {}
+
+ /**
+ * No-op since this source is read-only.
+ *
+ * @param name The property name (ignored).
+ */
+ @Override
+ public void unset(String name) {}
+
+ /**
+ * No-op since this source is read-only.
+ */
+ @Override
+ public void clear() {}
+
+}
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/SettingSource.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/SettingSource.java
new file mode 100644
index 0000000000..09b88f96d6
--- /dev/null
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/SettingSource.java
@@ -0,0 +1,119 @@
+/*
+ * 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.juneau.commons.settings;
+
+import java.util.*;
+
+/**
+ * Interface for pluggable property sources used by {@link Settings}.
+ *
+ * <p>
+ * A setting source provides a way to retrieve and optionally modify property
values.
+ * Sources are checked in reverse order (last added is checked first) when
looking up properties.
+ *
+ * <h5 class='section'>Return Value Semantics:</h5>
+ * <ul class='spaced-list'>
+ * <li><c>null</c> - The setting does not exist in this source. The lookup
will continue to the next source.
+ * <li><c>Optional.empty()</c> - The setting exists but has an explicitly
null value. This will be returned
+ * immediately, overriding any values from lower-priority sources.
+ * <li><c>Optional.of(value)</c> - The setting exists and has a non-null
value. This will be returned immediately.
+ * </ul>
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Create a writable source</jc>
+ * MapSource <jv>source</jv> = <jk>new</jk> MapSource();
+ * <jv>source</jv>.set(<js>"my.property"</js>, <js>"value"</js>);
+ *
+ * <jc>// Create a read-only source from a function</jc>
+ * ReadOnlySource <jv>readOnly</jv> = <jk>new</jk> ReadOnlySource(x ->
System.getProperty(x));
+ * </p>
+ */
+public interface SettingSource {
+
+ /**
+ * Returns a setting in this setting source.
+ *
+ * <p>
+ * Return value semantics:
+ * <ul>
+ * <li><c>null</c> - The setting does not exist in this source.
The lookup will continue to the next source.
+ * <li><c>Optional.empty()</c> - The setting exists but has an
explicitly null value. This will be returned
+ * immediately, overriding any values from lower-priority
sources.
+ * <li><c>Optional.of(value)</c> - The setting exists and has a
non-null value. This will be returned immediately.
+ * </ul>
+ *
+ * @param name The property name.
+ * @return The property value, <c>null</c> if the property doesn't
exist in this source, or <c>Optional.empty()</c>
+ * if the property exists but has a null value.
+ */
+ Optional<String> get(String name);
+
+ /**
+ * Returns whether this source is writable.
+ *
+ * <p>
+ * If <c>false</c>, all write operations ({@link #set(String, String)},
{@link #unset(String)}, {@link #clear()})
+ * should be no-ops.
+ *
+ * @return <c>true</c> if this source is writable, <c>false</c>
otherwise.
+ */
+ boolean canWrite();
+
+ /**
+ * Sets a setting in this setting source.
+ *
+ * <p>
+ * Should be a no-op if the source is not writable (i.e., {@link
#canWrite()} returns <c>false</c>).
+ *
+ * <p>
+ * Setting a value to <c>null</c> means that {@link #get(String)} will
return <c>Optional.empty()</c> for that key,
+ * effectively overriding any values from lower-priority sources. Use
{@link #unset(String)} if you want
+ * {@link #get(String)} to return <c>null</c> (indicating the key
doesn't exist in this source).
+ *
+ * @param name The property name.
+ * @param value The property value, or <c>null</c> to set an empty
override.
+ */
+ void set(String name, String value);
+
+ /**
+ * Removes a setting from this setting source.
+ *
+ * <p>
+ * Should be a no-op if the source is not writable (i.e., {@link
#canWrite()} returns <c>false</c>).
+ *
+ * <p>
+ * After calling this method, {@link #get(String)} will return
<c>null</c> for the specified key,
+ * indicating that the key doesn't exist in this source (as opposed to
returning <c>Optional.empty()</c>,
+ * which would indicate the key exists but has a null value).
+ *
+ * @param name The property name to remove.
+ */
+ void unset(String name);
+
+ /**
+ * Clears all settings from this setting source.
+ *
+ * <p>
+ * Should be a no-op if the source is not writable (i.e., {@link
#canWrite()} returns <c>false</c>).
+ *
+ * <p>
+ * After calling this method, all keys will be removed from this
source, and {@link #get(String)} will
+ * return <c>null</c> for all keys.
+ */
+ void clear();
+}
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/Settings.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/Settings.java
new file mode 100644
index 0000000000..b57b2d3188
--- /dev/null
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/Settings.java
@@ -0,0 +1,533 @@
+/*
+ * 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.juneau.commons.settings;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.io.*;
+import java.net.*;
+import java.nio.charset.*;
+import java.nio.file.*;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Encapsulates Java system properties with support for global and per-thread
overrides for unit testing.
+ *
+ * <p>
+ * This class provides a thread-safe way to access system properties that can
be overridden at both the global level
+ * and per-thread level, making it useful for unit tests that need to
temporarily change system property
+ * values without affecting other tests or threads.
+ *
+ * <h5 class='section'>Lookup Order:</h5>
+ * <p>
+ * When retrieving a property value, the lookup order is:
+ * <ol>
+ * <li>Per-thread override (if set via {@link #setLocal(String, String)})
+ * <li>Global override (if set via {@link #setGlobal(String, String)})
+ * <li>Sources in reverse order (last source added via {@link
#addSource(SettingSource)} is checked first)
+ * <li>System property source (default, always second-to-last)
+ * <li>System environment variable source (default, always last)
+ * </ol>
+ *
+ * <h5 class='section'>Features:</h5>
+ * <ul class='spaced-list'>
+ * <li>System property access - read Java system properties with type
conversion
+ * <li>Global overrides - override system properties globally for all
threads
+ * <li>Per-thread overrides - override system properties for specific
threads (useful for unit tests)
+ * <li>Custom sources - add arbitrary property sources (e.g., Spring
properties, environment variables, config files)
+ * <li>Disable override support - system property to prevent new global
overrides from being set
+ * <li>Type-safe accessors - convenience methods for common types:
Integer, Long, Boolean, Double, Float, File, Path, URI, Charset
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Get a system property as a string</jc>
+ * Optional<String> <jv>value</jv> =
Settings.<jsf>get</jsf>().get(<js>"my.property"</js>);
+ *
+ * <jc>// Get with type conversion</jc>
+ * Optional<Integer> <jv>intValue</jv> =
Settings.<jsf>get</jsf>().getInteger(<js>"my.int.property"</js>);
+ * Optional<Long> <jv>longValue</jv> =
Settings.<jsf>get</jsf>().getLong(<js>"my.long.property"</js>);
+ * Optional<Boolean> <jv>boolValue</jv> =
Settings.<jsf>get</jsf>().getBoolean(<js>"my.bool.property"</js>);
+ * Optional<Double> <jv>doubleValue</jv> =
Settings.<jsf>get</jsf>().getDouble(<js>"my.double.property"</js>);
+ * Optional<Float> <jv>floatValue</jv> =
Settings.<jsf>get</jsf>().getFloat(<js>"my.float.property"</js>);
+ * Optional<File> <jv>fileValue</jv> =
Settings.<jsf>get</jsf>().getFile(<js>"my.file.property"</js>);
+ * Optional<Path> <jv>pathValue</jv> =
Settings.<jsf>get</jsf>().getPath(<js>"my.path.property"</js>);
+ * Optional<URI> <jv>uriValue</jv> =
Settings.<jsf>get</jsf>().getURI(<js>"my.uri.property"</js>);
+ * Optional<Charset> <jv>charsetValue</jv> =
Settings.<jsf>get</jsf>().getCharset(<js>"my.charset.property"</js>);
+ *
+ * <jc>// Override for current thread (useful in unit tests)</jc>
+ * Settings.<jsf>get</jsf>().setLocal(<js>"my.property"</js>,
<js>"test-value"</js>);
+ * <jc>// ... test code that uses the override ...</jc>
+ * Settings.<jsf>get</jsf>().unsetLocal(<js>"my.property"</js>); <jc>//
Remove specific override</jc>
+ * <jc>// OR</jc>
+ * Settings.<jsf>get</jsf>().clearLocal(); <jc>// Clear all thread-local
overrides</jc>
+ *
+ * <jc>// Set global override (applies to all threads)</jc>
+ * Settings.<jsf>get</jsf>().setGlobal(<js>"my.property"</js>,
<js>"global-value"</js>);
+ * <jc>// ... code that uses the global override ...</jc>
+ * Settings.<jsf>get</jsf>().unsetGlobal(<js>"my.property"</js>); <jc>//
Remove specific override</jc>
+ * <jc>// OR</jc>
+ * Settings.<jsf>get</jsf>().clearGlobal(); <jc>// Clear all global
overrides</jc>
+ *
+ * <jc>// Add a custom source (e.g., Spring properties)</jc>
+ * MapSource <jv>springSource</jv> = <jk>new</jk> MapSource();
+ * <jv>springSource</jv>.set(<js>"spring.datasource.url"</js>,
<js>"jdbc:postgresql://localhost/db"</js>);
+ * Settings.<jsf>get</jsf>().addSource(<jv>springSource</jv>);
+ * </p>
+ *
+ * <h5 class='section'>System Properties:</h5>
+ * <ul class='spaced-list'>
+ * <li><c>juneau.settings.disableGlobal</c> - If set to <c>true</c>,
prevents new global overrides
+ * from being set via {@link #setGlobal(String, String)}. Existing
global overrides will still be
+ * returned by {@link #get(String)} until explicitly removed.
+ * <li><c>juneau.settings.disableCustomSources</c> - If set to
<c>true</c>, prevents custom sources
+ * from being added via {@link #addSource(SettingSource)} or
{@link #setSources(SettingSource...)}.
+ * </ul>
+ */
+public class Settings {
+
+ private static final Settings INSTANCE = new Settings();
+
+ /**
+ * Returns the singleton instance of Settings.
+ *
+ * @return The singleton Settings instance.
+ */
+ public static Settings get() {
+ return INSTANCE;
+ }
+
+ private final MapSource globalOverrides = new MapSource();
+ private final ThreadLocal<MapSource> threadOverrides = new
ThreadLocal<>();
+ private final List<SettingSource> sources = new
CopyOnWriteArrayList<>();
+
+ /**
+ * System property source that delegates to {@link
System#getProperty(String)}.
+ */
+ public static final SettingSource SYSTEM_PROPERTY_SOURCE = new
ReadOnlySource(x -> System.getProperty(x));
+
+ /**
+ * System environment variable source that delegates to {@link
System#getenv(String)}.
+ */
+ public static final SettingSource SYSTEM_ENV_SOURCE = new
ReadOnlySource(x -> System.getenv(x));
+
+ /**
+ * Returns properties for this Settings object itself.
+ * Note that these are initialized at startup and not changeable
through System.setProperty().
+ */
+ private static final Optional<String> initProperty(String property) {
+ var v = SYSTEM_PROPERTY_SOURCE.get(property);
+ if (v != null)
+ return v;
+ v = SYSTEM_ENV_SOURCE.get(property);
+ if (v != null)
+ return v;
+ return opte();
+ }
+
+ private static final String DISABLE_CUSTOM_SOURCES_PROP =
"juneau.settings.disableCustomSources";
+ private static final String DISABLE_GLOBAL_PROP =
"juneau.settings.disableGlobal";
+ private static final boolean DISABLE_CUSTOM_SOURCES =
initProperty(DISABLE_CUSTOM_SOURCES_PROP).map(Boolean::valueOf).orElse(false);
+ private static final boolean DISABLE_GLOBAL =
initProperty(DISABLE_GLOBAL_PROP).map(Boolean::valueOf).orElse(false);
+
+ /**
+ * Constructor.
+ */
+ private Settings() {
+ // Initialize with system sources as defaults (env first, then
property, so property has higher precedence)
+ sources.add(SYSTEM_ENV_SOURCE);
+ sources.add(SYSTEM_PROPERTY_SOURCE);
+ }
+
+ /**
+ * Returns the value of the specified system property.
+ *
+ * <p>
+ * The lookup order is:
+ * <ol>
+ * <li>Per-thread override (if set via {@link #setLocal(String,
String)})
+ * <li>Global override (if set via {@link #setGlobal(String,
String)})
+ * <li>Sources in reverse order (last source added via {@link
#addSource(SettingSource)} is checked first)
+ * <li>System property source (default, always second-to-last)
+ * <li>System environment variable source (default, always last)
+ * </ol>
+ *
+ * @param name The property name.
+ * @return The property value, or {@link Optional#empty()} if not found.
+ */
+ public Optional<String> get(String name) {
+ // 1. Check thread-local override
+ var localSource = threadOverrides.get();
+ if (localSource != null) {
+ var v = localSource.get(name);
+ if (v != null)
+ return v; // v is Optional.empty() if key
exists with null value, or Optional.of(value) if present
+ }
+
+ // 2. Check global override
+ var v = globalOverrides.get(name);
+ if (v != null)
+ return v; // v is Optional.empty() if key exists with
null value, or Optional.of(value) if present
+
+ // 3. Check sources in reverse order (last added first)
+ for (int i = sources.size() - 1; i >= 0; i--) {
+ var source = sources.get(i);
+ if (source == null)
+ continue; // Skip null sources (defensive check)
+ var result = source.get(name);
+ if (result == null)
+ continue; // Key not in this source, try next
source
+ // If result is Optional.empty(), it means key exists
with null value, so return it
+ // If result is present, return it
+ return result;
+ }
+
+ return Optional.empty();
+ }
+
+ /**
+ * Returns the value of the specified system property as an Integer.
+ *
+ * <p>
+ * The property value is parsed using {@link Integer#valueOf(String)}.
If the property is not found
+ * or cannot be parsed as an integer, returns {@link Optional#empty()}.
+ *
+ * @param name The property name.
+ * @return The property value as an Integer, or {@link
Optional#empty()} if not found or not a valid integer.
+ */
+ public Optional<Integer> getInteger(String name) {
+ return get(name).map(v ->
safeOrNull(()->Integer.valueOf(v))).filter(Objects::nonNull);
+ }
+
+ /**
+ * Returns the value of the specified system property as a Long.
+ *
+ * <p>
+ * The property value is parsed using {@link Long#valueOf(String)}. If
the property is not found
+ * or cannot be parsed as a long, returns {@link Optional#empty()}.
+ *
+ * @param name The property name.
+ * @return The property value as a Long, or {@link Optional#empty()} if
not found or not a valid long.
+ */
+ public Optional<Long> getLong(String name) {
+ return get(name).map(v ->
safeOrNull(()->Long.valueOf(v))).filter(Objects::nonNull);
+ }
+
+ /**
+ * Returns the value of the specified system property as a Boolean.
+ *
+ * <p>
+ * The property value is parsed using {@link
Boolean#parseBoolean(String)}, which returns <c>true</c>
+ * if the value is (case-insensitive) "true", otherwise <c>false</c>.
Note that this method will
+ * return <c>Optional.of(false)</c> for any non-empty value that is not
"true", and
+ * {@link Optional#empty()} only if the property is not set.
+ *
+ * @param name The property name.
+ * @return The property value as a Boolean, or {@link Optional#empty()}
if not found.
+ */
+ public Optional<Boolean> getBoolean(String name) {
+ return get(name).map(v -> Boolean.parseBoolean(v));
+ }
+
+ /**
+ * Returns the value of the specified system property as a Double.
+ *
+ * <p>
+ * The property value is parsed using {@link Double#valueOf(String)}.
If the property is not found
+ * or cannot be parsed as a double, returns {@link Optional#empty()}.
+ *
+ * @param name The property name.
+ * @return The property value as a Double, or {@link Optional#empty()}
if not found or not a valid double.
+ */
+ public Optional<Double> getDouble(String name) {
+ return get(name).map(v ->
safeOrNull(()->Double.valueOf(v))).filter(Objects::nonNull);
+ }
+
+ /**
+ * Returns the value of the specified system property as a Float.
+ *
+ * <p>
+ * The property value is parsed using {@link Float#valueOf(String)}. If
the property is not found
+ * or cannot be parsed as a float, returns {@link Optional#empty()}.
+ *
+ * @param name The property name.
+ * @return The property value as a Float, or {@link Optional#empty()}
if not found or not a valid float.
+ */
+ public Optional<Float> getFloat(String name) {
+ return get(name).map(v ->
safeOrNull(()->Float.valueOf(v))).filter(Objects::nonNull);
+ }
+
+ /**
+ * Returns the value of the specified system property as a File.
+ *
+ * <p>
+ * The property value is converted to a {@link File} using the {@link
File#File(String)} constructor.
+ * If the property is not found, returns {@link Optional#empty()}. Note
that this method does not
+ * validate that the file path is valid or that the file exists.
+ *
+ * @param name The property name.
+ * @return The property value as a File, or {@link Optional#empty()} if
not found.
+ */
+ public Optional<File> getFile(String name) {
+ return get(name).map(v -> new File(v));
+ }
+
+ /**
+ * Returns the value of the specified system property as a Path.
+ *
+ * <p>
+ * The property value is converted to a {@link Path} using {@link
Paths#get(String, String...)}.
+ * If the property is not found or the path string is invalid, returns
{@link Optional#empty()}.
+ *
+ * @param name The property name.
+ * @return The property value as a Path, or {@link Optional#empty()} if
not found or not a valid path.
+ */
+ public Optional<Path> getPath(String name) {
+ return get(name).map(v ->
safeOrNull(()->Paths.get(v))).filter(Objects::nonNull);
+ }
+
+ /**
+ * Returns the value of the specified system property as a URI.
+ *
+ * <p>
+ * The property value is converted to a {@link URI} using {@link
URI#create(String)}.
+ * If the property is not found or the URI string is invalid, returns
{@link Optional#empty()}.
+ *
+ * @param name The property name.
+ * @return The property value as a URI, or {@link Optional#empty()} if
not found or not a valid URI.
+ */
+ public Optional<URI> getURI(String name) {
+ return get(name).map(v ->
safeOrNull(()->URI.create(v))).filter(Objects::nonNull);
+ }
+
+ /**
+ * Returns the value of the specified system property as a Charset.
+ *
+ * <p>
+ * The property value is converted to a {@link Charset} using {@link
Charset#forName(String)}.
+ * If the property is not found or the charset name is not supported,
returns {@link Optional#empty()}.
+ *
+ * @param name The property name.
+ * @return The property value as a Charset, or {@link Optional#empty()}
if not found or not a valid charset.
+ */
+ public Optional<Charset> getCharset(String name) {
+ return get(name).map(v ->
safeOrNull(()->Charset.forName(v))).filter(Objects::nonNull);
+ }
+
+ /**
+ * Sets a global override for the specified property.
+ *
+ * <p>
+ * This override will apply to all threads and takes precedence over
system properties.
+ * However, per-thread overrides (set via {@link #setLocal(String,
String)}) will still take precedence
+ * over global overrides.
+ *
+ * <p>
+ * If the <c>juneau.settings.disableGlobal</c> system property is set
to <c>true</c>,
+ * this method will throw an exception. This allows system
administrators
+ * to prevent applications from overriding system properties globally.
+ *
+ * <p>
+ * Setting a value to <c>null</c> will store an empty optional,
effectively overriding the system
+ * property to return empty. Use {@link #unsetGlobal(String)} to
completely remove the override.
+ *
+ * @param name The property name.
+ * @param value The override value, or <c>null</c> to set an empty
override.
+ * @see #unsetGlobal(String)
+ * @see #clearGlobal()
+ */
+ public void setGlobal(String name, String value) {
+ assertArg(! DISABLE_GLOBAL, "Global settings have been disabled
via ''{0}''", DISABLE_GLOBAL_PROP);
+ globalOverrides.set(name, value);
+ }
+
+ /**
+ * Removes a global override for the specified property.
+ *
+ * <p>
+ * After calling this method, the property will fall back to the system
property value
+ * (or per-thread override if one exists).
+ *
+ * @param name The property name.
+ * @see #setGlobal(String, String)
+ * @see #clearGlobal()
+ */
+ public void unsetGlobal(String name) {
+ assertArg(! DISABLE_GLOBAL, "Global settings have been disabled
via ''{0}''", DISABLE_GLOBAL_PROP);
+ globalOverrides.unset(name);
+ }
+
+ /**
+ * Sets a per-thread override for the specified property.
+ *
+ * <p>
+ * This override will only apply to the current thread and takes
precedence over global overrides
+ * and system properties. This is particularly useful in unit tests
where you need to temporarily
+ * change a system property value without affecting other threads or
tests.
+ *
+ * <p>
+ * Setting a value to <c>null</c> will store an empty optional,
effectively overriding the property
+ * to return empty for this thread. Use {@link #unsetLocal(String)} to
completely remove the override.
+ *
+ * @param name The property name.
+ * @param value The override value, or <c>null</c> to set an empty
override.
+ * @see #unsetLocal(String)
+ * @see #clearLocal()
+ */
+ public void setLocal(String name, String value) {
+ var localSource = threadOverrides.get();
+ if (localSource == null) {
+ localSource = new MapSource();
+ threadOverrides.set(localSource);
+ }
+ localSource.set(name, value);
+ }
+
+ /**
+ * Removes a per-thread override for the specified property.
+ *
+ * <p>
+ * After calling this method, the property will fall back to the global
override (if set)
+ * or the system property value for the current thread.
+ *
+ * @param name The property name.
+ * @see #setLocal(String, String)
+ * @see #clearLocal()
+ */
+ public void unsetLocal(String name) {
+ var localSource = threadOverrides.get();
+ if (localSource != null)
+ localSource.unset(name);
+ }
+
+ /**
+ * Clears all per-thread overrides for the current thread.
+ *
+ * <p>
+ * After calling this method, all properties will fall back to global
overrides (if set)
+ * or system property values for the current thread.
+ *
+ * <p>
+ * This is typically called in a <c>@AfterEach</c> or <c>@After</c>
test method to clean up
+ * thread-local overrides after a test completes.
+ *
+ * @see #setLocal(String, String)
+ * @see #unsetLocal(String)
+ */
+ public void clearLocal() {
+ threadOverrides.remove();
+ }
+
+ /**
+ * Clears all global overrides.
+ *
+ * <p>
+ * After calling this method, all properties will fall back to resolver
values
+ * (or per-thread overrides if they exist).
+ *
+ * @see #setGlobal(String, String)
+ * @see #unsetGlobal(String)
+ */
+ public void clearGlobal() {
+ globalOverrides.clear();
+ }
+
+ /**
+ * Adds a source to the source list.
+ *
+ * <p>
+ * Sources are checked in reverse order (last added is checked first),
after global overrides
+ * but before the system property source. This allows you to add custom
property sources such
+ * as Spring properties, environment variables, or configuration files.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Add a Spring properties source</jc>
+ * MapSource <jv>springSource</jv> = <jk>new</jk> MapSource();
+ * <jv>springSource</jv>.set(<js>"spring.datasource.url"</js>,
<js>"jdbc:postgresql://localhost/db"</js>);
+ * Settings.<jsf>get</jsf>().addSource(<jv>springSource</jv>);
+ * </p>
+ *
+ * @param source The source. Must not be <c>null</c>.
+ * @return This object for method chaining.
+ * @see #setSources(SettingSource...)
+ */
+ public Settings addSource(SettingSource source) {
+ assertArg(! DISABLE_CUSTOM_SOURCES, "Global custom sources have
been disabled via ''{0}''", DISABLE_CUSTOM_SOURCES_PROP);
+ assertArgNotNull("source", source);
+ // Add at the end - since we iterate in reverse order, this
means it's checked first (before system sources)
+ sources.add(source);
+ return this;
+ }
+
+ /**
+ * Sets the source list, replacing all existing sources.
+ *
+ * <p>
+ * This method clears all existing sources (except the system property
and environment variable sources which are always
+ * present) and adds the specified sources in order. The sources will
be checked in reverse
+ * order (last source in the array is checked first).
+ *
+ * <p>
+ * Note that using this method resets existing sources, so if you want
to maintain resolving environment variables
+ * or system properties, you'll need to add {@link #SYSTEM_ENV_SOURCE}
and {@link #SYSTEM_PROPERTIES_SOURCE} to the
+ * list of sources added to this method.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Set multiple sources</jc>
+ * MapSource <jv>springSource</jv> = <jk>new</jk> MapSource();
+ * MapSource <jv>envSource</jv> = <jk>new</jk> MapSource();
+ * Settings.<jsf>get</jsf>().setSources(<jv>springSource</jv>,
<jv>envSource</jv>);
+ * <jc>// Lookup order: local > global > envSource >
springSource > system properties > system env vars</jc>
+ * </p>
+ *
+ * <p>
+ * Note that the order of precedence of the sources is in reverse
order. So sources at the end of the list are
+ * checked before sources at the beginning of the list.
+ *
+ * @param sources The sources to add. Must not be <c>null</c> or
contain <c>null</c> elements.
+ * @return This object for method chaining.
+ * @see #addSource(SettingSource)
+ */
+ @SafeVarargs
+ public final Settings setSources(SettingSource...sources) {
+ assertArg(! DISABLE_CUSTOM_SOURCES, "Global custom sources have
been disabled via ''{0}''", DISABLE_CUSTOM_SOURCES_PROP);
+ assertVarargsNotNull("sources", sources);
+ // Remove all sources
+ this.sources.clear();
+ // Add new sources
+ for (var source : sources) {
+ this.sources.add(source);
+ }
+ return this;
+ }
+
+ /**
+ * Resets the sources list to default (only system sources).
+ * Package-private for testing purposes.
+ */
+ void resetSources() {
+ this.sources.clear();
+ this.sources.add(SYSTEM_ENV_SOURCE);
+ this.sources.add(SYSTEM_PROPERTY_SOURCE);
+ }
+}
+
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Utils.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Utils.java
index 3a67bf6090..510fe76bdc 100644
---
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Utils.java
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Utils.java
@@ -1519,6 +1519,45 @@ public class Utils {
}
}
+ /**
+ * Executes a supplier that may throw an exception and returns the
result or <c>null</c>.
+ *
+ * <p>
+ * If the supplier executes successfully, returns the result.
+ * If the supplier throws any exception, returns <c>null</c>.
+ *
+ * <p>
+ * This is useful for operations that may fail but you want to handle
the failure
+ * gracefully by returning <c>null</c> instead of throwing an
exception. This is particularly
+ * useful in fluent method chains where you want to filter out failed
conversions.
+ *
+ * <p>
+ * This method is similar to {@link #safeOpt(ThrowingSupplier)} but
returns <c>null</c> instead
+ * of <c>Optional.empty()</c> when an exception occurs. Use this method
when you prefer <c>null</c>
+ * over <c>Optional</c> for error handling.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Parse an integer, returning null if parsing fails</jc>
+ * Integer <jv>value</jv> = <jsm>safeOrNull</jsm>(() ->
Integer.valueOf(<js>"123"</js>)); <jc>// 123</jc>
+ * Integer <jv>invalid</jv> = <jsm>safeOrNull</jsm>(() ->
Integer.valueOf(<js>"abc"</js>)); <jc>// null</jc>
+ *
+ * <jc>// Use in a fluent chain to filter out failed
conversions</jc>
+ * Optional<Integer> <jv>parsed</jv> =
get(<js>"my.property"</js>)
+ * .map(<jv>v</jv> -> <jsm>safeOrNull</jsm>(() ->
Integer.valueOf(<jv>v</jv>)))
+ * .filter(Objects::nonNull);
+ * </p>
+ *
+ * @param <T> The return type.
+ * @param s The supplier that may throw an exception.
+ * @return The result of the supplier if successful, or <c>null</c> if
an exception was thrown.
+ * @see #safeOpt(ThrowingSupplier)
+ * @see #safe(ThrowingSupplier)
+ */
+ public static <T> T safeOrNull(ThrowingSupplier<T> s) {
+ return safeOpt(s).orElse(null);
+ }
+
/**
* Allows you to wrap a supplier that throws an exception so that it
can be used in a fluent interface.
*
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/settings/Settings_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/settings/Settings_Test.java
new file mode 100644
index 0000000000..bae91ec0b2
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/commons/settings/Settings_Test.java
@@ -0,0 +1,613 @@
+/*
+ * 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.juneau.commons.settings;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.File;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+class Settings_Test extends TestBase {
+
+ private static final String TEST_PROP = "juneau.test.property";
+ private static final String TEST_PROP_2 = "juneau.test.property2";
+
+ @BeforeEach
+ void setUp() {
+ // Clean up before each test
+ Settings.get().clearLocal();
+ Settings.get().clearGlobal();
+ Settings.get().resetSources();
+ // Remove test system properties if they exist
+ System.clearProperty(TEST_PROP);
+ System.clearProperty(TEST_PROP_2);
+ }
+
+ @AfterEach
+ void tearDown() {
+ // Clean up after each test
+ Settings.get().clearLocal();
+ Settings.get().clearGlobal();
+ System.clearProperty(TEST_PROP);
+ System.clearProperty(TEST_PROP_2);
+ }
+
+
//====================================================================================================
+ // get() - Basic functionality
+
//====================================================================================================
+ @Test
+ void a01_get_fromSystemProperty() {
+ System.setProperty(TEST_PROP, "system-value");
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("system-value", result.get());
+ }
+
+ @Test
+ void a02_get_notFound() {
+ Optional<String> result =
Settings.get().get("nonexistent.property");
+ assertFalse(result.isPresent());
+ }
+
+ @Test
+ void a03_get_fromGlobalOverride() {
+ Settings.get().setGlobal(TEST_PROP, "global-value");
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("global-value", result.get());
+ }
+
+ @Test
+ void a04_get_fromLocalOverride() {
+ Settings.get().setLocal(TEST_PROP, "local-value");
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("local-value", result.get());
+ }
+
+ @Test
+ void a05_get_lookupOrder_localOverridesGlobal() {
+ System.setProperty(TEST_PROP, "system-value");
+ Settings.get().setGlobal(TEST_PROP, "global-value");
+ Settings.get().setLocal(TEST_PROP, "local-value");
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("local-value", result.get());
+ }
+
+ @Test
+ void a06_get_lookupOrder_globalOverridesSystem() {
+ System.setProperty(TEST_PROP, "system-value");
+ Settings.get().setGlobal(TEST_PROP, "global-value");
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("global-value", result.get());
+ }
+
+ @Test
+ void a07_get_nullValue() {
+ Settings.get().setLocal(TEST_PROP, null);
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertFalse(result.isPresent());
+ }
+
+
//====================================================================================================
+ // getInteger()
+
//====================================================================================================
+ @Test
+ void b01_getInteger_valid() {
+ System.setProperty(TEST_PROP, "123");
+ Optional<Integer> result = Settings.get().getInteger(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals(123, result.get());
+ }
+
+ @Test
+ void b02_getInteger_invalid() {
+ System.setProperty(TEST_PROP, "not-a-number");
+ Optional<Integer> result = Settings.get().getInteger(TEST_PROP);
+ assertFalse(result.isPresent());
+ }
+
+ @Test
+ void b03_getInteger_notFound() {
+ Optional<Integer> result =
Settings.get().getInteger("nonexistent.property");
+ assertFalse(result.isPresent());
+ }
+
+ @Test
+ void b04_getInteger_fromOverride() {
+ Settings.get().setLocal(TEST_PROP, "456");
+ Optional<Integer> result = Settings.get().getInteger(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals(456, result.get());
+ }
+
+
//====================================================================================================
+ // getLong()
+
//====================================================================================================
+ @Test
+ void c01_getLong_valid() {
+ System.setProperty(TEST_PROP, "123456789");
+ Optional<Long> result = Settings.get().getLong(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals(123456789L, result.get());
+ }
+
+ @Test
+ void c02_getLong_invalid() {
+ System.setProperty(TEST_PROP, "not-a-number");
+ Optional<Long> result = Settings.get().getLong(TEST_PROP);
+ assertFalse(result.isPresent());
+ }
+
+ @Test
+ void c03_getLong_fromOverride() {
+ Settings.get().setLocal(TEST_PROP, "987654321");
+ Optional<Long> result = Settings.get().getLong(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals(987654321L, result.get());
+ }
+
+
//====================================================================================================
+ // getBoolean()
+
//====================================================================================================
+ @Test
+ void d01_getBoolean_true() {
+ System.setProperty(TEST_PROP, "true");
+ Optional<Boolean> result = Settings.get().getBoolean(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertTrue(result.get());
+ }
+
+ @Test
+ void d02_getBoolean_false() {
+ System.setProperty(TEST_PROP, "false");
+ Optional<Boolean> result = Settings.get().getBoolean(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertFalse(result.get());
+ }
+
+ @Test
+ void d03_getBoolean_caseInsensitive() {
+ System.setProperty(TEST_PROP, "TRUE");
+ Optional<Boolean> result = Settings.get().getBoolean(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertTrue(result.get());
+ }
+
+ @Test
+ void d04_getBoolean_nonTrueValue() {
+ System.setProperty(TEST_PROP, "anything");
+ Optional<Boolean> result = Settings.get().getBoolean(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertFalse(result.get());
+ }
+
+ @Test
+ void d05_getBoolean_notFound() {
+ Optional<Boolean> result =
Settings.get().getBoolean("nonexistent.property");
+ assertFalse(result.isPresent());
+ }
+
+
//====================================================================================================
+ // getDouble()
+
//====================================================================================================
+ @Test
+ void e01_getDouble_valid() {
+ System.setProperty(TEST_PROP, "123.456");
+ Optional<Double> result = Settings.get().getDouble(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals(123.456, result.get(), 0.0001);
+ }
+
+ @Test
+ void e02_getDouble_invalid() {
+ System.setProperty(TEST_PROP, "not-a-number");
+ Optional<Double> result = Settings.get().getDouble(TEST_PROP);
+ assertFalse(result.isPresent());
+ }
+
+
//====================================================================================================
+ // getFloat()
+
//====================================================================================================
+ @Test
+ void f01_getFloat_valid() {
+ System.setProperty(TEST_PROP, "123.456");
+ Optional<Float> result = Settings.get().getFloat(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals(123.456f, result.get(), 0.0001f);
+ }
+
+ @Test
+ void f02_getFloat_invalid() {
+ System.setProperty(TEST_PROP, "not-a-number");
+ Optional<Float> result = Settings.get().getFloat(TEST_PROP);
+ assertFalse(result.isPresent());
+ }
+
+
//====================================================================================================
+ // getFile()
+
//====================================================================================================
+ @Test
+ void g01_getFile_valid() {
+ System.setProperty(TEST_PROP, "/tmp/test.txt");
+ Optional<File> result = Settings.get().getFile(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals(new File("/tmp/test.txt"), result.get());
+ }
+
+ @Test
+ void g02_getFile_notFound() {
+ Optional<File> result =
Settings.get().getFile("nonexistent.property");
+ assertFalse(result.isPresent());
+ }
+
+
//====================================================================================================
+ // getPath()
+
//====================================================================================================
+ @Test
+ void h01_getPath_valid() {
+ System.setProperty(TEST_PROP, "/tmp/test.txt");
+ Optional<Path> result = Settings.get().getPath(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals(Paths.get("/tmp/test.txt"), result.get());
+ }
+
+ @Test
+ void h02_getPath_invalid() {
+ // Paths.get() can throw exceptions for invalid paths on some
systems
+ // This test verifies that invalid paths return empty
+ System.setProperty(TEST_PROP, "\0invalid");
+ Optional<Path> result = Settings.get().getPath(TEST_PROP);
+ // May or may not be empty depending on OS, but should not throw
+ assertNotNull(result);
+ }
+
+
//====================================================================================================
+ // getURI()
+
//====================================================================================================
+ @Test
+ void i01_getURI_valid() {
+ System.setProperty(TEST_PROP, "http://example.com/test");
+ Optional<URI> result = Settings.get().getURI(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals(URI.create("http://example.com/test"),
result.get());
+ }
+
+ @Test
+ void i02_getURI_invalid() {
+ System.setProperty(TEST_PROP, "not a valid uri");
+ Optional<URI> result = Settings.get().getURI(TEST_PROP);
+ assertFalse(result.isPresent());
+ }
+
+
//====================================================================================================
+ // getCharset()
+
//====================================================================================================
+ @Test
+ void j01_getCharset_valid() {
+ System.setProperty(TEST_PROP, "UTF-8");
+ Optional<Charset> result = Settings.get().getCharset(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals(Charset.forName("UTF-8"), result.get());
+ }
+
+ @Test
+ void j02_getCharset_invalid() {
+ System.setProperty(TEST_PROP, "INVALID-CHARSET");
+ Optional<Charset> result = Settings.get().getCharset(TEST_PROP);
+ assertFalse(result.isPresent());
+ }
+
+
//====================================================================================================
+ // setGlobal() / unsetGlobal() / clearGlobal()
+
//====================================================================================================
+ @Test
+ void k01_setGlobal() {
+ Settings.get().setGlobal(TEST_PROP, "global-value");
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("global-value", result.get());
+ }
+
+ @Test
+ void k02_unsetGlobal() {
+ Settings.get().setGlobal(TEST_PROP, "global-value");
+ Settings.get().unsetGlobal(TEST_PROP);
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertFalse(result.isPresent());
+ }
+
+ @Test
+ void k03_clearGlobal() {
+ Settings.get().setGlobal(TEST_PROP, "value1");
+ Settings.get().setGlobal(TEST_PROP_2, "value2");
+ Settings.get().clearGlobal();
+ assertFalse(Settings.get().get(TEST_PROP).isPresent());
+ assertFalse(Settings.get().get(TEST_PROP_2).isPresent());
+ }
+
+ @Test
+ void k04_setGlobal_nullValue() {
+ Settings.get().setGlobal(TEST_PROP, null);
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertFalse(result.isPresent());
+ }
+
+
//====================================================================================================
+ // setLocal() / unsetLocal() / clearLocal()
+
//====================================================================================================
+ @Test
+ void l01_setLocal() {
+ Settings.get().setLocal(TEST_PROP, "local-value");
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("local-value", result.get());
+ }
+
+ @Test
+ void l02_unsetLocal() {
+ Settings.get().setLocal(TEST_PROP, "local-value");
+ Settings.get().unsetLocal(TEST_PROP);
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertFalse(result.isPresent());
+ }
+
+ @Test
+ void l03_clearLocal() {
+ Settings.get().setLocal(TEST_PROP, "value1");
+ Settings.get().setLocal(TEST_PROP_2, "value2");
+ Settings.get().clearLocal();
+ assertFalse(Settings.get().get(TEST_PROP).isPresent());
+ assertFalse(Settings.get().get(TEST_PROP_2).isPresent());
+ }
+
+ @Test
+ void l04_setLocal_nullValue() {
+ Settings.get().setLocal(TEST_PROP, null);
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertFalse(result.isPresent());
+ }
+
+ @Test
+ void l05_setLocal_overridesGlobal() {
+ Settings.get().setGlobal(TEST_PROP, "global-value");
+ Settings.get().setLocal(TEST_PROP, "local-value");
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("local-value", result.get());
+ }
+
+ @Test
+ void l06_setLocal_overridesSystemProperty() {
+ System.setProperty(TEST_PROP, "system-value");
+ Settings.get().setLocal(TEST_PROP, "local-value");
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("local-value", result.get());
+ }
+
+
//====================================================================================================
+ // Thread isolation
+
//====================================================================================================
+ @Test
+ void m01_localOverride_threadIsolation() throws InterruptedException {
+ Settings.get().setLocal(TEST_PROP, "thread1-value");
+
+ Thread thread2 = new Thread(() -> {
+ Settings.get().setLocal(TEST_PROP, "thread2-value");
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("thread2-value", result.get());
+ });
+
+ thread2.start();
+ thread2.join();
+
+ // Original thread should still have its value
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("thread1-value", result.get());
+ }
+
+ @Test
+ void m02_globalOverride_sharedAcrossThreads() throws
InterruptedException {
+ Settings.get().setGlobal(TEST_PROP, "global-value");
+
+ Thread thread2 = new Thread(() -> {
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("global-value", result.get());
+ });
+
+ thread2.start();
+ thread2.join();
+
+ // Original thread should also see the global value
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("global-value", result.get());
+ }
+
+
//====================================================================================================
+ // Singleton pattern
+
//====================================================================================================
+ @Test
+ void n01_get_returnsSameInstance() {
+ Settings instance1 = Settings.get();
+ Settings instance2 = Settings.get();
+ assertSame(instance1, instance2);
+ }
+
+
//====================================================================================================
+ // Sources
+
//====================================================================================================
+ @Test
+ void o01_addSource() {
+ MapSource source = new MapSource();
+ source.set(TEST_PROP, "source-value");
+ Settings.get().addSource(source);
+
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("source-value", result.get());
+ }
+
+ @Test
+ void o02_addSource_reverseOrder() {
+ MapSource source1 = new MapSource();
+ source1.set(TEST_PROP, "source1-value");
+ Settings.get().addSource(source1);
+
+ MapSource source2 = new MapSource();
+ source2.set(TEST_PROP, "source2-value");
+ Settings.get().addSource(source2);
+
+ // Last added source should be checked first
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("source2-value", result.get());
+ }
+
+ @Test
+ void o03_addSource_afterGlobalOverride() {
+ Settings.get().setGlobal(TEST_PROP, "global-value");
+
+ MapSource source = new MapSource();
+ source.set(TEST_PROP, "source-value");
+ Settings.get().addSource(source);
+
+ // Global override should take precedence
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("global-value", result.get());
+ }
+
+ @Test
+ void o04_addSource_beforeSystemProperty() {
+ System.setProperty(TEST_PROP, "system-value");
+
+ MapSource source = new MapSource();
+ source.set(TEST_PROP, "source-value");
+ Settings.get().addSource(source);
+
+ // Source should take precedence over system property
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("source-value", result.get());
+ }
+
+ @Test
+ void o05_addSource_fallbackToSystemProperty() {
+ MapSource source = new MapSource();
+ // Source doesn't have the property
+
+ System.setProperty(TEST_PROP, "system-value");
+ Settings.get().addSource(source);
+
+ // Should fall back to system property
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("system-value", result.get());
+ }
+
+ @Test
+ void o06_setSources() {
+ MapSource source1 = new MapSource();
+ source1.set(TEST_PROP, "source1-value");
+
+ MapSource source2 = new MapSource();
+ source2.set(TEST_PROP, "source2-value");
+
+ Settings.get().setSources(source1, source2);
+
+ // Last source in array should be checked first
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("source2-value", result.get());
+ }
+
+ @Test
+ void o07_setSources_clearsExisting() {
+ MapSource source1 = new MapSource();
+ source1.set(TEST_PROP, "source1-value");
+ Settings.get().addSource(source1);
+
+ MapSource source2 = new MapSource();
+ source2.set(TEST_PROP, "source2-value");
+ Settings.get().setSources(source2);
+
+ // Only source2 should exist now
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertTrue(result.isPresent());
+ assertEquals("source2-value", result.get());
+ }
+
+ @Test
+ void o08_addSource_nullValue() {
+ MapSource source = new MapSource();
+ source.set(TEST_PROP, null);
+ Settings.get().addSource(source);
+
+ // Note that setting a null value on the source overrides the
system property.
+ System.setProperty(TEST_PROP, "system-value");
+ Optional<String> result = Settings.get().get(TEST_PROP);
+ assertFalse(result.isPresent());
+ }
+
+ @Test
+ void o09_addSource_nullSource() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ Settings.get().addSource(null);
+ });
+ }
+
+ @Test
+ void o10_setSources_nullSource() {
+ MapSource source1 = new MapSource();
+ assertThrows(IllegalArgumentException.class, () -> {
+ Settings.get().setSources(source1, null);
+ });
+ }
+
+ @Test
+ void o11_source_springPropertiesExample() {
+ // Simulate Spring properties
+ MapSource springSource = new MapSource();
+ springSource.set("spring.datasource.url",
"jdbc:postgresql://localhost/db");
+ springSource.set("spring.datasource.username", "admin");
+
+ Settings.get().addSource(springSource);
+
+ Optional<String> url =
Settings.get().get("spring.datasource.url");
+ assertTrue(url.isPresent());
+ assertEquals("jdbc:postgresql://localhost/db", url.get());
+
+ Optional<String> username =
Settings.get().get("spring.datasource.username");
+ assertTrue(username.isPresent());
+ assertEquals("admin", username.get());
+ }
+}
+