diff --git a/README.adoc b/README.adoc
index 9a227b6..39eb9bf 100644
--- a/README.adoc
+++ b/README.adoc
@@ -14,6 +14,7 @@ CNF-01 is a library that provides immutable configuration for Java projects. Imm
// List your project's features
- Provides immutable configurations for:
. configuration files / path properties (`PathConfiguration`)
+. System Properties (`PropertiesConfiguration`)
. environment variables (`EnvironmentConfiguration`)
- Default configurations in case the provided configurations from a source are not found or are otherwise broken (`DefaultConfiguration`)
diff --git a/src/main/java/com/teragrep/cnf_01/PropertiesConfiguration.java b/src/main/java/com/teragrep/cnf_01/PropertiesConfiguration.java
new file mode 100644
index 0000000..bb492b5
--- /dev/null
+++ b/src/main/java/com/teragrep/cnf_01/PropertiesConfiguration.java
@@ -0,0 +1,122 @@
+/*
+ * Teragrep Configuration Library for Java (cnf_01)
+ * Copyright (C) 2024 Suomen Kanuuna Oy
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ *
+ * Additional permission under GNU Affero General Public License version 3
+ * section 7
+ *
+ * If you modify this Program, or any covered work, by linking or combining it
+ * with other code, such other code is not for that reason alone subject to any
+ * of the requirements of the GNU Affero GPL version 3 as long as this Program
+ * is the same Program as licensed from Suomen Kanuuna Oy without any additional
+ * modifications.
+ *
+ * Supplemented terms under GNU Affero General Public License version 3
+ * section 7
+ *
+ * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified
+ * versions must be marked as "Modified version of" The Program.
+ *
+ * Names of the licensors and authors may not be used for publicity purposes.
+ *
+ * No rights are granted for use of trade names, trademarks, or service marks
+ * which are in The Program if any.
+ *
+ * Licensee must indemnify licensors and authors for any liability that these
+ * contractual assumptions impose on licensors and authors.
+ *
+ * To the extent this program is licensed as part of the Commercial versions of
+ * Teragrep, the applicable Commercial License may apply to this file if you as
+ * a licensee so wish it.
+ */
+package com.teragrep.cnf_01;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.stream.Collectors;
+
+/**
+ * Provides unmodifiable configuration for Java's Properties object. Uses System properties by default if the
+ * constructor is not given a parameter.
+ */
+public final class PropertiesConfiguration implements Configuration {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesConfiguration.class);
+
+ private final Map configuration;
+
+ public PropertiesConfiguration() {
+ this(System.getProperties());
+ }
+
+ public PropertiesConfiguration(final Properties properties) {
+ this(
+ Collections
+ .unmodifiableMap(
+ properties
+ .entrySet()
+ .stream()
+ .collect(
+ Collectors
+ .toMap(
+ entry -> entry.getKey().toString(),
+ entry -> entry.getValue().toString()
+ )
+ )
+ )
+ );
+ }
+
+ /**
+ * Private constructor so that a map can't be given as parameter by users. It would break immutability.
+ *
+ * @param configuration the configuration as an immutable Map
+ */
+ private PropertiesConfiguration(final Map configuration) {
+ this.configuration = configuration;
+ }
+
+ @Override
+ public Map asMap() {
+ LOGGER.debug("Returning configuration map generated from properties.");
+ LOGGER.trace("Returning configuration map <[{}]>", configuration);
+
+ return configuration;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ else if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final PropertiesConfiguration config = (PropertiesConfiguration) o;
+ return configuration.equals(config.configuration);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(configuration);
+ }
+}
diff --git a/src/test/java/com/teragrep/cnf_01/PropertiesConfigurationTest.java b/src/test/java/com/teragrep/cnf_01/PropertiesConfigurationTest.java
new file mode 100644
index 0000000..c0b0354
--- /dev/null
+++ b/src/test/java/com/teragrep/cnf_01/PropertiesConfigurationTest.java
@@ -0,0 +1,158 @@
+/*
+ * Teragrep Configuration Library for Java (cnf_01)
+ * Copyright (C) 2024 Suomen Kanuuna Oy
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ *
+ * Additional permission under GNU Affero General Public License version 3
+ * section 7
+ *
+ * If you modify this Program, or any covered work, by linking or combining it
+ * with other code, such other code is not for that reason alone subject to any
+ * of the requirements of the GNU Affero GPL version 3 as long as this Program
+ * is the same Program as licensed from Suomen Kanuuna Oy without any additional
+ * modifications.
+ *
+ * Supplemented terms under GNU Affero General Public License version 3
+ * section 7
+ *
+ * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified
+ * versions must be marked as "Modified version of" The Program.
+ *
+ * Names of the licensors and authors may not be used for publicity purposes.
+ *
+ * No rights are granted for use of trade names, trademarks, or service marks
+ * which are in The Program if any.
+ *
+ * Licensee must indemnify licensors and authors for any liability that these
+ * contractual assumptions impose on licensors and authors.
+ *
+ * To the extent this program is licensed as part of the Commercial versions of
+ * Teragrep, the applicable Commercial License may apply to this file if you as
+ * a licensee so wish it.
+ */
+package com.teragrep.cnf_01;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.Properties;
+
+public class PropertiesConfigurationTest {
+
+ @Test
+ public void testSystemProperties() {
+ System.setProperty("foo", "bar");
+ System.setProperty("bar", "foo");
+
+ PropertiesConfiguration config = new PropertiesConfiguration(); // uses System.getProperties()
+ // clearing properties doesn't affect PropertiesConfiguration
+ System.clearProperty("foo");
+ System.clearProperty("bar");
+
+ Map result = config.asMap();
+
+ Assertions.assertEquals("bar", result.get("foo"));
+ Assertions.assertEquals("foo", result.get("bar"));
+ }
+
+ @Test
+ public void testProperties() {
+ Properties properties = new Properties();
+ properties.put("foo", "bar");
+ properties.put("bar", "foo");
+
+ PropertiesConfiguration config = new PropertiesConfiguration(properties);
+ // modifying the original properties doesn't modify the result map
+ properties.put("biz", "buz");
+
+ Map result = config.asMap();
+
+ Assertions.assertEquals(2, result.size());
+ Assertions.assertEquals("bar", result.get("foo"));
+ Assertions.assertEquals("foo", result.get("bar"));
+ }
+
+ @Test
+ public void testEmptyProperties() {
+ Properties properties = new Properties();
+
+ PropertiesConfiguration config = new PropertiesConfiguration(properties);
+ Map result = config.asMap();
+
+ Assertions.assertEquals(0, result.size());
+ }
+
+ @Test
+ public void testEqualsWithSystemProperties() {
+ PropertiesConfiguration config1 = new PropertiesConfiguration();
+ PropertiesConfiguration config2 = new PropertiesConfiguration();
+
+ config1.asMap();
+ Assertions.assertEquals(config1, config2);
+ }
+
+ @Test
+ public void testEqualsWithProperties() {
+ Properties properties1 = new Properties();
+ properties1.put("foo", "bar");
+
+ Properties properties2 = new Properties();
+ properties2.put("foo", "bar");
+
+ PropertiesConfiguration config1 = new PropertiesConfiguration(properties1);
+ // modifying properties1 doesn't modify immutable PropertiesConfiguration
+ properties1.put("bar", "foo");
+
+ PropertiesConfiguration config2 = new PropertiesConfiguration(properties2);
+
+ config1.asMap();
+ Assertions.assertEquals(config1, config2);
+ }
+
+ @Test
+ public void testNotEquals() {
+ Properties properties = new Properties();
+ properties.put("foo", "bar");
+
+ PropertiesConfiguration config1 = new PropertiesConfiguration();
+ PropertiesConfiguration config2 = new PropertiesConfiguration(properties);
+
+ Assertions.assertNotEquals(config1, config2);
+ }
+
+ @Test
+ public void testHashCode() {
+ Properties properties1 = new Properties();
+ properties1.put("foo", "bar");
+
+ Properties properties2 = new Properties();
+ properties2.put("foo", "bar");
+
+ PropertiesConfiguration config1 = new PropertiesConfiguration(properties1);
+ PropertiesConfiguration config2 = new PropertiesConfiguration(properties2);
+ PropertiesConfiguration difConfig = new PropertiesConfiguration();
+
+ Assertions.assertEquals(config1.hashCode(), config2.hashCode());
+ Assertions.assertNotEquals(config1.hashCode(), difConfig.hashCode());
+ }
+
+ @Test
+ public void testEqualsVerifier() {
+ EqualsVerifier.forClass(PropertiesConfiguration.class).withNonnullFields("configuration").verify();
+ }
+}