diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..3dade41
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,44 @@
+# Changelog
+
+## 0.1.1 — 2025-09-13
+
+### Highlights
+- Fixes “URI with undefined scheme” when `phoenixd.base_url` lacks an HTTP scheme by defaulting to `http://`.
+- Centralizes configuration via shared `Configuration` util (env vars take precedence over `app.properties`).
+
+### Changes
+- fix: normalize base_url scheme and use shared Configuration
+ - F:phoenixd-rest/src/main/java/xyz/tcheeric/phoenixd/operation/AbstractOperation.java
+ - F:phoenixd-rest/src/test/java/xyz/tcheeric/phoenixd/operation/BaseUrlNormalizationTest.java
+ - F:phoenixd-test/src/test/java/xyz/tcheeric/phoenixd/test/TestUtils.java
+- docs: document scheme defaulting for `phoenixd.base_url`
+ - F:docs/reference/configuration.md
+- build: bump version to 0.1.1
+ - F:pom.xml
+ - F:phoenixd-rest/pom.xml
+ - F:phoenixd-test/pom.xml
+ - F:phoenixd-base/pom.xml
+ - F:phoenixd-model/pom.xml
+ - F:phoenixd-mock/pom.xml
+
+### Behavior Notes
+- `phoenixd.base_url` without a scheme now resolves as `http://...`.
+- If `phoenixd.base_url` is unset or blank, an `IllegalArgumentException` is thrown during request construction.
+
+### Configuration
+- Env vars (preferred): `PHOENIXD_USERNAME`, `PHOENIXD_PASSWORD`, `PHOENIXD_BASE_URL`, `PHOENIXD_TIMEOUT`.
+- Or `app.properties` on the classpath with `phoenixd.username`, `phoenixd.password`, `phoenixd.base_url`, `phoenixd.timeout`.
+
+### Testing
+- Command: `mvn -q verify`
+- Note: In restricted sandboxes, tests that open loopback sockets (MockWebServer) may fail with `SocketException: Operation not permitted`. In a normal dev/CI environment, the test suite is expected to pass.
+
+### API Changes
+- None.
+
+### Security
+- No new dependencies; no changes to security-sensitive logic.
+
+### Migration
+- No breaking changes. If you relied on rejecting schemeless base URLs, be aware they now default to `http://`.
+
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
index 8293d1b..30ae281 100644
--- a/docs/reference/configuration.md
+++ b/docs/reference/configuration.md
@@ -20,7 +20,7 @@ Configuration can also be supplied through an `app.properties` file on the class
|-----|--------|---------|-------|
| `phoenixd.username` | String | empty | Username for basic authentication. |
| `phoenixd.password` | String | empty | Password for basic authentication. Store securely. |
-| `phoenixd.base_url` | URL | empty | Base address of the API endpoint. |
+| `phoenixd.base_url` | URL | empty | Base address of the API endpoint. If no scheme is provided, `http://` is assumed. |
| `phoenixd.timeout` | Integer (ms) | `5000` | HTTP request timeout. |
| `phoenixd.webhook_secret` | String | empty | Optional secret used to validate webhook callbacks. Treat as a secret. |
diff --git a/phoenixd-base/pom.xml b/phoenixd-base/pom.xml
index 7e29dae..c34f125 100644
--- a/phoenixd-base/pom.xml
+++ b/phoenixd-base/pom.xml
@@ -4,7 +4,7 @@
xyz.tcheeric
phoenixd-java
- 0.1.0
+ 0.1.1
phoenixd-base
diff --git a/phoenixd-mock/pom.xml b/phoenixd-mock/pom.xml
index f849e14..f88d178 100644
--- a/phoenixd-mock/pom.xml
+++ b/phoenixd-mock/pom.xml
@@ -6,7 +6,7 @@
xyz.tcheeric
phoenixd-java
- 0.1.0
+ 0.1.1
phoenixd-mock
diff --git a/phoenixd-model/pom.xml b/phoenixd-model/pom.xml
index f4b3888..4770654 100644
--- a/phoenixd-model/pom.xml
+++ b/phoenixd-model/pom.xml
@@ -4,7 +4,7 @@
xyz.tcheeric
phoenixd-java
- 0.1.0
+ 0.1.1
phoenixd-model
diff --git a/phoenixd-rest/pom.xml b/phoenixd-rest/pom.xml
index 8514809..0a4c38f 100644
--- a/phoenixd-rest/pom.xml
+++ b/phoenixd-rest/pom.xml
@@ -4,12 +4,12 @@
xyz.tcheeric
phoenixd-java
- 0.1.0
+ 0.1.1
phoenixd-rest
jar
- 0.1.0
+ 0.1.1
phoenixd-rest
https://maven.apache.org
diff --git a/phoenixd-rest/src/main/java/xyz/tcheeric/phoenixd/operation/AbstractOperation.java b/phoenixd-rest/src/main/java/xyz/tcheeric/phoenixd/operation/AbstractOperation.java
index ca8cbbd..8d5bdce 100644
--- a/phoenixd-rest/src/main/java/xyz/tcheeric/phoenixd/operation/AbstractOperation.java
+++ b/phoenixd-rest/src/main/java/xyz/tcheeric/phoenixd/operation/AbstractOperation.java
@@ -6,6 +6,7 @@
import xyz.tcheeric.phoenixd.operation.impl.PostOperation;
import xyz.tcheeric.phoenixd.common.rest.Operation;
import xyz.tcheeric.phoenixd.common.rest.Request;
+import xyz.tcheeric.phoenixd.common.rest.util.Configuration;
import java.io.IOException;
import java.net.URI;
@@ -19,7 +20,6 @@
import java.util.Base64;
import java.util.List;
import java.util.Map;
-import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -32,34 +32,34 @@ public abstract class AbstractOperation implements Operation {
private String requestData;
public static final long DEFAULT_TIMEOUT = 5000L;
- private static final String PREFIX = "phoenixd.";
- private static final Properties CONFIG = loadConfig();
-
- @SneakyThrows
- private static Properties loadConfig() {
- Properties props = new Properties();
- try (var stream = AbstractOperation.class.getResourceAsStream("/app.properties")) {
- if (stream != null) {
- props.load(stream);
- }
- }
- return props;
- }
+ private static final String PREFIX = "phoenixd";
+ private static final Configuration CONFIG = new Configuration(PREFIX);
private static String getProperty(String key) {
- return CONFIG.getProperty(PREFIX + key);
+ return CONFIG.get(key);
}
private static long getLongProperty(String key, long defaultValue) {
- String value = CONFIG.getProperty(PREFIX + key);
- if (value == null) {
- return defaultValue;
+ Long value = CONFIG.getLong(key, defaultValue);
+ return value != null ? value : defaultValue;
+ }
+
+ private static String ensureScheme(String baseUrl) {
+ if (baseUrl == null || baseUrl.isBlank()) {
+ throw new IllegalArgumentException("phoenixd.base_url is not set");
}
- try {
- return Long.parseLong(value);
- } catch (NumberFormatException e) {
- return defaultValue;
+ String trimmed = baseUrl.trim();
+ String lower = trimmed.toLowerCase();
+ if (lower.startsWith("http://") || lower.startsWith("https://")) {
+ return trimmed;
}
+ return "http://" + trimmed;
+ }
+
+ private static URI buildUri(String baseUrl, String path) {
+ String normalizedBase = ensureScheme(baseUrl);
+ String normalizedPath = (path == null || path.isEmpty()) ? "/" : (path.startsWith("/") ? path : "/" + path);
+ return URI.create(normalizedBase).resolve(normalizedPath);
}
public AbstractOperation(@NonNull HttpRequest httpRequest) {
@@ -80,7 +80,7 @@ public AbstractOperation(@NonNull String method, @NonNull String path, String re
HttpRequest.BodyPublisher bodyPublisher = requestData == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(requestData);
this.httpRequest = HttpRequest.newBuilder()
- .uri(URI.create(baseUrl + path))
+ .uri(buildUri(baseUrl, path))
.header("Authorization", "Basic " + encodedAuth)
.timeout(Duration.ofMillis(timeout))
.method(method, bodyPublisher)
@@ -107,7 +107,7 @@ public AbstractOperation(@NonNull String method, @NonNull String path, @NonNull
}
this.httpRequest = HttpRequest.newBuilder()
- .uri(URI.create(baseUrl + resolvedPath))
+ .uri(buildUri(baseUrl, resolvedPath))
.header("Authorization", "Basic " + encodedAuth)
.timeout(Duration.ofMillis(timeout))
.method(method, bodyPublisher)
diff --git a/phoenixd-rest/src/test/java/xyz/tcheeric/phoenixd/operation/BaseUrlNormalizationTest.java b/phoenixd-rest/src/test/java/xyz/tcheeric/phoenixd/operation/BaseUrlNormalizationTest.java
new file mode 100644
index 0000000..a0a402d
--- /dev/null
+++ b/phoenixd-rest/src/test/java/xyz/tcheeric/phoenixd/operation/BaseUrlNormalizationTest.java
@@ -0,0 +1,28 @@
+package xyz.tcheeric.phoenixd.operation;
+
+import org.junit.jupiter.api.Test;
+import xyz.tcheeric.phoenixd.common.rest.util.Configuration;
+import xyz.tcheeric.phoenixd.operation.impl.PostOperation;
+
+import java.lang.reflect.Field;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BaseUrlNormalizationTest {
+
+ // Verifies base_url without scheme defaults to http
+ @Test
+ void baseUrlWithoutSchemeDefaultsToHttp() throws Exception {
+ Field configField = AbstractOperation.class.getDeclaredField("CONFIG");
+ configField.setAccessible(true);
+ Configuration cfg = (Configuration) configField.get(null);
+ cfg.getProperties().setProperty("phoenixd.base_url", "localhost:9999");
+ cfg.getProperties().setProperty("phoenixd.username", "user");
+ cfg.getProperties().setProperty("phoenixd.password", "pass");
+
+ PostOperation op = new PostOperation("/items", "data");
+ assertThat(op.getHttpRequest().uri().getScheme()).isEqualTo("http");
+ assertThat(op.getHttpRequest().uri().toString()).isEqualTo("http://localhost:9999/items");
+ }
+}
+
diff --git a/phoenixd-test/pom.xml b/phoenixd-test/pom.xml
index 3080763..94051b2 100644
--- a/phoenixd-test/pom.xml
+++ b/phoenixd-test/pom.xml
@@ -4,7 +4,7 @@
xyz.tcheeric
phoenixd-java
- 0.1.0
+ 0.1.1
phoenixd-test
diff --git a/phoenixd-test/src/test/java/xyz/tcheeric/phoenixd/test/TestUtils.java b/phoenixd-test/src/test/java/xyz/tcheeric/phoenixd/test/TestUtils.java
index e250a35..6f7fc8b 100644
--- a/phoenixd-test/src/test/java/xyz/tcheeric/phoenixd/test/TestUtils.java
+++ b/phoenixd-test/src/test/java/xyz/tcheeric/phoenixd/test/TestUtils.java
@@ -1,9 +1,9 @@
package xyz.tcheeric.phoenixd.test;
import xyz.tcheeric.phoenixd.operation.AbstractOperation;
+import xyz.tcheeric.phoenixd.common.rest.util.Configuration;
import java.lang.reflect.Field;
-import java.util.Properties;
public final class TestUtils {
private static final String CONFIG_FIELD_NAME = "CONFIG";
@@ -16,8 +16,14 @@ public static void setBaseUrl(String baseUrl) {
try {
Field configField = AbstractOperation.class.getDeclaredField(CONFIG_FIELD_NAME);
configField.setAccessible(true);
- Properties props = (Properties) configField.get(null);
- props.setProperty(PHOENIXD_BASE_URL_KEY, baseUrl);
+ Object cfg = configField.get(null);
+ if (cfg instanceof Configuration configuration) {
+ configuration.getProperties().setProperty(PHOENIXD_BASE_URL_KEY, baseUrl);
+ } else if (cfg instanceof java.util.Properties props) {
+ props.setProperty(PHOENIXD_BASE_URL_KEY, baseUrl);
+ } else {
+ throw new IllegalStateException("Unsupported CONFIG type: " + (cfg == null ? "null" : cfg.getClass()));
+ }
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
diff --git a/pom.xml b/pom.xml
index f5a01c4..20565db 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
xyz.tcheeric
phoenixd-java
- 0.1.0
+ 0.1.1
pom