diff --git a/gradle.properties b/gradle.properties index e9fc9b8f85..9d84e22f7d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ mavenPassword=YourPassword # here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/LibPsql.kt (adminVersion property) # Warning: Only update LibPsql version, if there is a change in SQL functions! # The reason is, that this version is encoded in the database, and when updated, forced an upgrade! -version=3.0.0-beta.35 +version=3.0.0-beta.36 org.gradle.jvmargs=-Xmx12g kotlin.code.style=official diff --git a/here-naksha-app-service/src/jvmMain/resources/swagger/openapi.yaml b/here-naksha-app-service/src/jvmMain/resources/swagger/openapi.yaml index 75214463b4..0d5ee57b35 100644 --- a/here-naksha-app-service/src/jvmMain/resources/swagger/openapi.yaml +++ b/here-naksha-app-service/src/jvmMain/resources/swagger/openapi.yaml @@ -12,7 +12,7 @@ servers: info: title: "Naskha Hub-API" description: "Naksha Hub-API is a REST API to provide simple access to geo data." - version: "3.0.0-beta.35" + version: "3.0.0-beta.36" security: - AccessToken: [ ] diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaVersion.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaVersion.kt index ae4c5fe9b1..a6077efb24 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaVersion.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaVersion.kt @@ -89,7 +89,7 @@ class NakshaVersion( * The current version as string to constant usage cases. * @since 3.0 */ - const val CURRENT = "3.0.0-beta.35" + const val CURRENT = "3.0.0-beta.36" // WARNING: Do not update this property manually, it is automatically modified when building! // Edit version only in `gradle.properties` file, which is used as well to create artifacts! diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorage.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorage.java index ce4f260700..0c0d8dec3c 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorage.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorage.java @@ -62,14 +62,7 @@ protected void initStorage(@NotNull NakshaStorage config, @Nullable Boolean crea if (httpStorageProperties == null || httpStorageProperties.getUrl() == null) { throw new IllegalArgumentException("A HTTP storage must have properties containing a 'url'"); } - this.defaultKeyProperties = new KeyProperties( - config.getId(), - httpStorageProperties.getUrl(), - httpStorageProperties.getHeaders(), - httpStorageProperties.getConnectTimeout(), - httpStorageProperties.getSocketTimeout(), - httpStorageProperties.getMaxRetries() - ); + this.defaultKeyProperties = KeyProperties.fromHttpStorageProperties(config.getId(), httpStorageProperties); } @NotNull diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageProperties.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageProperties.java index 904c56906b..7a993d2184 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageProperties.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageProperties.java @@ -18,6 +18,7 @@ */ package com.here.naksha.storage.http; +import naksha.base.JvmMapProxy; import naksha.model.NakshaError; import naksha.model.NakshaException; import naksha.model.NakshaVersion; @@ -35,6 +36,16 @@ @AvailableSince(NakshaVersion.v2_0_12) public class HttpStorageProperties extends NakshaProperties { + static final class HeaderMap extends JvmMapProxy { + private HeaderMap() { + super(String.class, String.class); + } + + private void putHeaders(@NotNull Map headers) { + headers.forEach(this::put); + } + } + public static final Integer DEF_CONNECTION_TIMEOUT_SEC = 20; public static final Integer DEF_SOCKET_TIMEOUT_SEC = 90; public static final Integer DEF_MAX_RETRIES = 1; @@ -105,11 +116,29 @@ public void setMaxRetries(final @Nullable Integer maxRetries) { * By default: 'Content-Type: application/json' and 'Accept-Encoding: gzip' */ public @NotNull Map getHeaders() { - return getOrSet(HEADERS, DEFAULT_HEADERS); + final Object raw = get(HEADERS); + if (raw instanceof HeaderMap) { + return (HeaderMap) raw; + } + if (raw instanceof Map) { + HeaderMap headers = toHeaderMap((Map) raw); + setRaw(HEADERS, headers); + return headers; + } + HeaderMap headers = new HeaderMap(); + headers.putHeaders(DEFAULT_HEADERS); + setRaw(HEADERS, headers); + return headers; } public void setHeaders(final @Nullable Map headers) { - setRaw(HEADERS, headers); + if (headers == null) { + setRaw(HEADERS, null); + return; + } + HeaderMap headerMap = new HeaderMap(); + headerMap.putHeaders(headers); + setRaw(HEADERS, headerMap); } public @NotNull HttpInterface getProtocol() { @@ -134,4 +163,14 @@ public void setHeaders(final @Nullable Map headers) { public void setProtocol(final HttpInterface protocol) {setRaw(HTTP_INTERFACE, protocol);} + private @NotNull HeaderMap toHeaderMap(@NotNull Map rawHeaders) { + HeaderMap headers = new HeaderMap(); + rawHeaders.forEach((key, value) -> { + if (key instanceof String && value instanceof String) { + headers.put((String) key, (String) value); + } + }); + return headers; + } + } diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/RequestSender.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/RequestSender.java index a52ebdbe46..7b439dca93 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/RequestSender.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/RequestSender.java @@ -165,6 +165,19 @@ public KeyProperties( this.maxRetries = maxRetries; } + public static @NotNull KeyProperties fromHttpStorageProperties( + @NotNull String name, + @NotNull HttpStorageProperties properties) { + return new KeyProperties( + name, + properties.getUrl(), + properties.getHeaders(), + properties.getConnectTimeout(), + properties.getSocketTimeout(), + properties.getMaxRetries() + ); + } + public @NotNull String getName() { return name; } diff --git a/here-naksha-storage-http/src/jvmTest/java/com/here/naksha/storage/http/HttpStoragePropertiesTest.java b/here-naksha-storage-http/src/jvmTest/java/com/here/naksha/storage/http/HttpStoragePropertiesTest.java index 74bdb5b369..138b17a26a 100644 --- a/here-naksha-storage-http/src/jvmTest/java/com/here/naksha/storage/http/HttpStoragePropertiesTest.java +++ b/here-naksha-storage-http/src/jvmTest/java/com/here/naksha/storage/http/HttpStoragePropertiesTest.java @@ -1,11 +1,23 @@ package com.here.naksha.storage.http; import org.junit.jupiter.api.Test; +import naksha.base.JvmBoxingUtil; +import naksha.base.JvmJsonUtil; +import naksha.base.Platform; +import naksha.model.objects.NakshaStorage; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; class HttpStoragePropertiesTest { + private static final String TEST_RESOURCE_DIR = "/unit_test_data/HttpStorageProperties/"; + @Test void shouldReturnDefaultValuesOnCreation() { // Given: a new HttpStorageProperties object created with the default constructor @@ -39,4 +51,160 @@ void should_set_and_get_all_properties_correctly() { assertEquals(testSocketTimeout, properties.getSocketTimeout()); assertEquals(testMaxRetries, properties.getMaxRetries()); } -} \ No newline at end of file + + @Test + void shouldNormalizeRawHeadersMapFromBoxedProperties() { + final HttpStorageProperties properties = new HttpStorageProperties(); + final Map rawHeaders = new HashMap<>(); + rawHeaders.put("Authorization", "Bearer "); + rawHeaders.put("Content-Type", "application/json"); + + // Simulate the v3 boxed/proxy state before the typed getter runs its normalization logic. + properties.setRaw("headers", rawHeaders); + + final Map headers = properties.getHeaders(); + assertEquals("Bearer ", headers.get("Authorization")); + assertEquals("application/json", headers.get("Content-Type")); + assertEquals(2, headers.size()); + assertEquals(headers, properties.getRaw("headers")); + assertTrue(headers instanceof HttpStorageProperties.HeaderMap); + assertInstanceOf(Map.class, properties.getRaw("headers")); + assertNotSame(rawHeaders, headers); + } + + @Test + void shouldStoreHeadersAsProxyWhenUsingSetter() { + final HttpStorageProperties properties = new HttpStorageProperties(); + final Map headers = Map.of( + "Authorization", "Bearer exampleToken", + "Content-Type", "application/json" + ); + + properties.setHeaders(headers); + + assertEquals(headers, properties.getHeaders()); + assertTrue(properties.getHeaders() instanceof HttpStorageProperties.HeaderMap); + assertInstanceOf(Map.class, properties.getRaw("headers")); + assertNotSame(headers, properties.getRaw("headers")); + } + + @Test + void shouldReturnDefaultsForInvalidRawValues() { + final HttpStorageProperties properties = new HttpStorageProperties(); + + properties.setRaw("headers", "invalid"); + + assertEquals(HttpStorageProperties.DEFAULT_HEADERS, properties.getHeaders()); + } + + @Test + void shouldDeserializeAllFieldsFromJson() { + final HttpStorageProperties properties = jsonResourceToPropertiesOrFail("t01_testConvertAllFields"); + + assertEquals("https://example.org", properties.getUrl()); + assertEquals(60, properties.getConnectTimeout()); + assertEquals(3600, properties.getSocketTimeout()); + + final Map headers = properties.getHeaders(); + assertEquals("Bearer ", headers.get("Authorization")); + assertEquals("application/json", headers.get("Content-Type")); + assertEquals(2, headers.size()); + } + + @Test + void shouldPreserveHeadersWhenBoxingStoragePropertiesFromJson() { + final String storageJson; + try { + storageJson = jsonResourceToStringOrFail("t05_testBoxStorageProperties"); + } catch (IOException e) { + fail("Unable to convert json resource", e); + return; + } + + final NakshaStorage storage = JvmBoxingUtil.box(Platform.fromJSON(storageJson), NakshaStorage.class); + assertNotNull(storage); + + final HttpStorageProperties properties = JvmBoxingUtil.box(storage.getProperties(), HttpStorageProperties.class); + assertNotNull(properties); + + final Object rawHeaders = properties.get("headers"); + assertInstanceOf(Map.class, rawHeaders); + assertFalse(rawHeaders instanceof HttpStorageProperties.HeaderMap); + + final Map headers = properties.getHeaders(); + assertEquals("Bearer boxed-token", headers.get("Authorization")); + assertEquals("demo", headers.get("X-Tenant")); + assertFalse(headers.containsKey("Accept-Encoding")); + assertEquals(2, headers.size()); + assertTrue(headers instanceof HttpStorageProperties.HeaderMap); + } + + @Test + void shouldDeserializeMissingValuesToDefaultsFromJson() { + final HttpStorageProperties properties = jsonResourceToPropertiesOrFail("t02_testConvertMissingToNull"); + + assertEquals("https://example.org", properties.getUrl()); + assertEquals(HttpStorageProperties.DEF_CONNECTION_TIMEOUT_SEC, properties.getConnectTimeout()); + assertEquals(HttpStorageProperties.DEF_SOCKET_TIMEOUT_SEC, properties.getSocketTimeout()); + assertEquals(HttpStorageProperties.DEF_MAX_RETRIES, properties.getMaxRetries()); + assertEquals(HttpStorageProperties.DEFAULT_HEADERS, properties.getHeaders()); + } + + @Test + void shouldIgnoreExcessFieldsInJson() { + assertDoesNotThrow(() -> jsonResourceToPropertiesOrFail("t03_testDontThrowOnExcessFields")); + } + + @Test + void shouldDeserializeMissingUrlAsNullInJson() { + final HttpStorageProperties properties = jsonResourceToPropertiesOrFail("t04_testThrowOnMissingMandatory"); + + assertNull(properties.getUrl()); + assertEquals(60, properties.getConnectTimeout()); + assertEquals(3600, properties.getSocketTimeout()); + assertEquals("Bearer ", properties.getHeaders().get("Authorization")); + assertEquals("application/json", properties.getHeaders().get("Content-Type")); + } + + @Test + void shouldUseNormalizedHeadersMapInKeyProperties() { + final HttpStorageProperties properties = new HttpStorageProperties(); + final Map rawHeaders = new HashMap<>(); + rawHeaders.put("Authorization", "Bearer "); + rawHeaders.put("Content-Type", "application/json"); + properties.setUrl("https://example.org"); + properties.setRaw("headers", rawHeaders); + + final RequestSender.KeyProperties fromProperties = RequestSender.KeyProperties.fromHttpStorageProperties("test-storage", properties); + + assertNotNull(fromProperties.getDefaultHeaders()); + assertTrue(fromProperties.getDefaultHeaders() instanceof HttpStorageProperties.HeaderMap); + assertNotSame(rawHeaders, fromProperties.getDefaultHeaders()); + assertEquals("Bearer ", fromProperties.getDefaultHeaders().get("Authorization")); + assertEquals("application/json", fromProperties.getDefaultHeaders().get("Content-Type")); + assertEquals(2, fromProperties.getDefaultHeaders().size()); + } + + private HttpStorageProperties jsonResourceToPropertiesOrFail(String fileName) { + try { + String json = jsonResourceToStringOrFail(fileName); + HttpStorageProperties properties = JvmJsonUtil.readJsonAs(json, HttpStorageProperties.class); + assertNotNull(properties); + return properties; + } catch (IOException e) { + fail("Unable to convert json resource", e); + return null; + } + } + + private String jsonResourceToStringOrFail(String fileName) throws IOException { + String resource = TEST_RESOURCE_DIR + fileName + ".json"; + + try (InputStream testResourceStream = this.getClass().getResourceAsStream(resource)) { + if (testResourceStream == null) { + throw new IOException("Could not access " + resource + " resource"); + } + return new String(testResourceStream.readAllBytes(), StandardCharsets.UTF_8); + } + } +} diff --git a/here-naksha-storage-http/src/jvmTest/resources/unit_test_data/HttpStorageProperties/t05_testBoxStorageProperties.json b/here-naksha-storage-http/src/jvmTest/resources/unit_test_data/HttpStorageProperties/t05_testBoxStorageProperties.json new file mode 100644 index 0000000000..3f81d732b6 --- /dev/null +++ b/here-naksha-storage-http/src/jvmTest/resources/unit_test_data/HttpStorageProperties/t05_testBoxStorageProperties.json @@ -0,0 +1,12 @@ +{ + "id": "http-storage", + "type": "Storage", + "className": "com.here.naksha.storage.http.HttpStorage", + "properties": { + "url": "https://example.org", + "headers": { + "Authorization": "Bearer boxed-token", + "X-Tenant": "demo" + } + } +}