-
Notifications
You must be signed in to change notification settings - Fork 0
feat(core): add base64 decode JSLT custom function #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
7fa2a4c
feat(mapping): implement entity dynamic mapping dry-run workflow for …
foukou19 79c6f34
Merge branch 'main' into feat/idp-core-entity-dynamic-mapping-dry-run
foukou19 844a44b
feat(entity-dynamic-mapping): update entity relation
foukou19 f741d07
feat(entity-dynamic-mapping): update entity relation
foukou19 e766205
feat(entity-dynamic-mapping): update entity relation
foukou19 fc62a02
feat(entity-dynamic-mapping): update swagger.yaml
foukou19 ffa6350
feat(core): init camel ingestion route
brandPittCode ca258db
feat(core): init camel ingestion route
brandPittCode e8ea98b
feat(core): add ingestion mode to mapping and ingestion processor
brandPittCode b1a6d31
feat(core): add ingestion mode to mapping and ingestion processor
brandPittCode cfc4d0e
Merge branch 'main' into feat/add-http-ingestion-route
brandPittCode cc0f30e
Merge branch 'main' into feat/add-http-ingestion-route
foukou19 46a174d
feat(core): improve coverage
foukou19 24efb34
feat(core): improve coverage
foukou19 4bc03ca
feat(core): remove webhook tracing implementation
foukou19 2eec39f
Merge branch 'main' into feat/add-http-ingestion-route
foukou19 8be5834
feat(core): improve coverage
foukou19 1bcb3b2
Merge branch 'main' into feat/add-http-ingestion-route
foukou19 50d6f17
feat(core): create dedicated spring security filter chain for webhooks
foukou19 08e5a05
feat(core): create dedicated spring security filter chain for webhooks
foukou19 10ae1f9
feat(core): add custom jslt function for base64 decoding
brandPittCode 0141f0c
Merge branch 'feat/add-http-ingestion-route' into feat/add-custom-bas…
brandPittCode fa1f355
Merge branch 'main' into feat/add-http-ingestion-route
foukou19 2a82b8e
feat(core): add custom jslt function for base64 decoding
brandPittCode ef958b9
Merge branch 'feat/add-http-ingestion-route' into feat/add-custom-bas…
brandPittCode b656f4e
Merge branch 'main' into feat/add-custom-base64-jstl-function
brandPittCode 03b0d25
feat(core): add custom jslt function for base64 decoding
brandPittCode 9749e8b
feat(core): add custom jslt function for base64 decoding
brandPittCode 6143055
feat(core): add custom jslt function for base64 decoding
brandPittCode f4db65a
feat(core): add custom jslt function for base64 decoding
brandPittCode 1e0e937
feat(core): add custom jslt function for base64 decoding
brandPittCode 21340da
feat(core): add custom jslt function for base64 decoding
brandPittCode 6683b40
feat(core): add custom jslt function for base64 decoding
brandPittCode File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
...ecathlon/idp_core/infrastructure/adapters/entity_mapping/jslt/functions/DecodeBase64.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| package com.decathlon.idp_core.infrastructure.adapters.entity_mapping.jslt.functions; | ||
|
|
||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Base64; | ||
|
|
||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.fasterxml.jackson.databind.node.NullNode; | ||
| import com.fasterxml.jackson.databind.node.TextNode; | ||
| import com.schibsted.spt.data.jslt.Function; | ||
|
|
||
| /// JSLT custom function that decodes a Base64-encoded string from an input | ||
| /// payload. | ||
| /// | ||
| /// **Usage in JSLT:** `base64-decode(<base64-encoded-string>)` | ||
| /// | ||
| /// Returns `null` if the input is absent, null, or blank. | ||
| @Component | ||
| public final class DecodeBase64 implements Function { | ||
|
|
||
| public static final String FUNCTION_NAME = "base64-decode"; | ||
| private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); | ||
|
|
||
| // Accepts exactly one argument: the Base64-encoded string node | ||
| private static final int MIN_ARGUMENTS = 1; | ||
| private static final int MAX_ARGUMENTS = 1; | ||
|
|
||
| @Override | ||
| public String getName() { | ||
| return FUNCTION_NAME; | ||
| } | ||
|
|
||
| @Override | ||
| public int getMinArguments() { | ||
| return MIN_ARGUMENTS; | ||
| } | ||
|
|
||
| @Override | ||
| public int getMaxArguments() { | ||
| return MAX_ARGUMENTS; | ||
| } | ||
|
|
||
| /// Decodes a Base64-encoded `JsonNode` string argument. | ||
| /// | ||
| /// **Behavior:** | ||
| /// - Returns `NullNode` if the input is absent, null, or blank | ||
| /// - Attempts to parse the decoded string as JSON; falls back to plain text if | ||
| /// parsing fails | ||
| /// - Throws `IllegalArgumentException` if the argument is non-textual (e.g., | ||
| /// number, object, array) | ||
| /// - Throws `IllegalArgumentException` if the Base64 string is malformed | ||
| /// | ||
| /// @param input the JSLT input context (unused) | ||
| /// @param args array of JsonNode arguments; expects exactly one text node | ||
| /// containing Base64-encoded data | ||
| /// @return a `JsonNode` (either parsed JSON, plain `TextNode`, or `NullNode`) | ||
| /// @throws IllegalArgumentException if arg is not textual or contains invalid | ||
| /// Base64 | ||
| @Override | ||
| public JsonNode call(JsonNode input, JsonNode[] args) { | ||
| if (args == null || args.length == 0) { | ||
| return NullNode.getInstance(); | ||
| } | ||
|
|
||
| JsonNode arg = args[0]; | ||
|
|
||
| // 1. Graceful: Null or absent inputs return NullNode | ||
| if (arg == null || arg.isNull()) { | ||
| return NullNode.getInstance(); | ||
| } | ||
|
|
||
| // 2. Strict: Non-textual types (Numbers, Objects, Arrays) throw an exception | ||
| if (!arg.isTextual()) { | ||
| throw new IllegalArgumentException( | ||
| "DecodeBase64 expects a string argument, but received: " + arg.getNodeType()); | ||
| } | ||
|
|
||
| // 3. Graceful: Blank strings return NullNode | ||
| String textValue = arg.asText(); | ||
|
brandPittCode marked this conversation as resolved.
|
||
| if (textValue.isBlank()) { | ||
| return NullNode.getInstance(); | ||
| } | ||
|
|
||
| // 4. Strict: Malformed Base64 strings throw an exception | ||
| try { | ||
| byte[] decodedBytes = Base64.getDecoder().decode(textValue); | ||
|
brandPittCode marked this conversation as resolved.
|
||
| String decodedString = new String(decodedBytes, StandardCharsets.UTF_8); | ||
| return parseDecodedString(decodedString); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new IllegalArgumentException("Invalid Base64 string payload: '" + textValue + "'", e); | ||
|
brandPittCode marked this conversation as resolved.
brandPittCode marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
|
||
| private JsonNode parseDecodedString(String decodedString) { | ||
| // Try parsing as structured JSON first | ||
| try { | ||
| JsonNode parsed = OBJECT_MAPPER.readTree(decodedString); | ||
| // Jackson returns Java null if decodedString is empty ("") | ||
| return (parsed != null) ? parsed : TextNode.valueOf(decodedString); | ||
| } catch (JsonProcessingException _) { | ||
| // Fallback to plain TextNode if the decoded string is not JSON | ||
| return TextNode.valueOf(decodedString); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
...hlon/idp_core/infrastructure/adapters/entity_mapping/jslt/functions/DecodeBase64Test.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| package com.decathlon.idp_core.infrastructure.adapters.entity_mapping.jslt.functions; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import com.decathlon.idp_core.domain.exception.entity_dynamic_mapping.EntityDynamicMappingJsltErrorException; | ||
| import com.decathlon.idp_core.infrastructure.adapters.entity_mapping.jslt.JsltEngine; | ||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.fasterxml.jackson.databind.node.IntNode; | ||
| import com.fasterxml.jackson.databind.node.NullNode; | ||
| import com.fasterxml.jackson.databind.node.TextNode; | ||
|
|
||
| // Unit test for the DecodeBase64 JSLT custom function, | ||
| // covering direct invocation and integration with the JsltEngine. | ||
| @DisplayName("DecodeBase64") | ||
| class DecodeBase64Test { | ||
|
|
||
| private DecodeBase64 decodeBase64; | ||
| private JsltEngine jsltEngine; | ||
| private ObjectMapper objectMapper; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| decodeBase64 = new DecodeBase64(); | ||
| jsltEngine = new JsltEngine(List.of(decodeBase64)); | ||
| objectMapper = new ObjectMapper(); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should return correct metadata signature") | ||
| void testMetadata() { | ||
| assertEquals("base64-decode", decodeBase64.getName()); | ||
| assertEquals(1, decodeBase64.getMinArguments()); | ||
| assertEquals(1, decodeBase64.getMaxArguments()); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should successfully decode a valid Base64 string directly") | ||
| void testDirectDecodeValidBase64() { | ||
| // "SGVsbG8gV29ybGQ=" is Base64 for "Hello World" | ||
| JsonNode context = NullNode.getInstance(); | ||
| JsonNode[] args = new JsonNode[]{TextNode.valueOf("SGVsbG8gV29ybGQ=")}; | ||
|
|
||
| JsonNode result = decodeBase64.call(context, args); | ||
|
|
||
| assertNotNull(result); | ||
| assertTrue(result.isTextual()); | ||
| assertEquals("Hello World", result.asText()); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should gracefully return NullNode for null, absent, or blank inputs") | ||
| void testDirectDecodeGracefulNullAndBlank() { | ||
| JsonNode context = NullNode.getInstance(); | ||
|
|
||
| // Null array element | ||
| JsonNode[] nullElementArgs = new JsonNode[]{null}; | ||
| assertEquals(NullNode.getInstance(), decodeBase64.call(context, nullElementArgs)); | ||
|
|
||
| // NullNode instance | ||
| JsonNode[] nullNodeArgs = new JsonNode[]{NullNode.getInstance()}; | ||
| assertEquals(NullNode.getInstance(), decodeBase64.call(context, nullNodeArgs)); | ||
|
|
||
| // Blank string | ||
| JsonNode[] blankStringArgs = new JsonNode[]{TextNode.valueOf(" ")}; | ||
| assertEquals(NullNode.getInstance(), decodeBase64.call(context, blankStringArgs)); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should throw IllegalArgumentException when input is not a textual type") | ||
| void testDirectDecodeWrongTypeThrowsException() { | ||
| JsonNode context = NullNode.getInstance(); | ||
| JsonNode[] nonTextualArgs = new JsonNode[]{new IntNode(123)}; | ||
|
|
||
| assertThrows(IllegalArgumentException.class, () -> decodeBase64.call(context, nonTextualArgs)); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should throw IllegalArgumentException when string payload is malformed Base64") | ||
| void testDirectDecodeMalformedBase64ThrowsException() { | ||
| JsonNode context = NullNode.getInstance(); | ||
| JsonNode[] malformedBase64Args = new JsonNode[]{TextNode.valueOf("NotValidBase64!!!")}; | ||
|
|
||
| assertThrows(IllegalArgumentException.class, | ||
| () -> decodeBase64.call(context, malformedBase64Args)); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should execute DecodeBase64 function inside JSLT engine evaluation successfully") | ||
| void testJsltEngineIntegrationSuccess() throws Exception { | ||
| String jsltExpression = "{ \"decoded\": base64-decode(.encoded_data) }"; | ||
| JsonNode payload = objectMapper.readTree("{\"encoded_data\": \"SGVsbG8=\"}"); // "SGVsbG8=" -> | ||
| // "Hello" | ||
|
|
||
| JsonNode result = jsltEngine.evaluate(jsltExpression, payload); | ||
|
|
||
| assertNotNull(result); | ||
| assertNotNull(result.get("decoded")); | ||
| assertTrue(result.get("decoded").isTextual()); | ||
| assertEquals("Hello", result.get("decoded").asText()); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should wrap Base64 decoding exception in EntityDynamicMappingJsltErrorException during engine evaluation") | ||
| void testJsltEngineIntegrationFailure() throws Exception { | ||
| String jsltExpression = "{ \"decoded\": base64-decode(.encoded_data) }"; | ||
| JsonNode payloadWithInvalidBase64 = objectMapper | ||
| .readTree("{\"encoded_data\": \"NotValidBase64!!!\"}"); | ||
|
|
||
| assertThrows(EntityDynamicMappingJsltErrorException.class, | ||
| () -> jsltEngine.evaluate(jsltExpression, payloadWithInvalidBase64)); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.