This is an automated email from the ASF dual-hosted git repository.
ahuber pushed a commit to branch v4
in repository https://gitbox.apache.org/repos/asf/causeway.git
The following commit(s) were added to refs/heads/v4 by this push:
new 0389d7326e5 CAUSEWAY-3889: adds DataUri representing the data URI
scheme
0389d7326e5 is described below
commit 0389d7326e548c4f5a4f6488fd9d81636074c3fd
Author: a.huber <[email protected]>
AuthorDate: Sun Aug 17 21:37:28 2025 +0200
CAUSEWAY-3889: adds DataUri representing the data URI scheme
see https://en.wikipedia.org/wiki/Data_URI_scheme
---
.../org/apache/causeway/applib/value/DataUri.java | 150 +++++++++++++++++++++
.../apache/causeway/applib/value/DataUriTest.java | 68 ++++++++++
2 files changed, 218 insertions(+)
diff --git
a/api/applib/src/main/java/org/apache/causeway/applib/value/DataUri.java
b/api/applib/src/main/java/org/apache/causeway/applib/value/DataUri.java
new file mode 100644
index 00000000000..bef78784e54
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/causeway/applib/value/DataUri.java
@@ -0,0 +1,150 @@
+/*
+ * 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.causeway.applib.value;
+
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.IntStream;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.util.StringUtils;
+
+import lombok.SneakyThrows;
+
+/**
+ * The data URI scheme is a uniform resource identifier (URI) scheme that
provides
+ * a way to include data in-line in Web pages as if they were external
resources.
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/Data_URI_scheme">wikipedia</a>
+ * @since 4.0
+ */
+public record DataUri(
+ String mediaType,
+ List<String> parameters,
+ Encoding encoding,
+ byte[] data) {
+
+ public enum Encoding {
+ NONE,
+ BASE64
+ }
+
+ @SneakyThrows
+ public static DataUri parse(String dataURI) {
+ var uri = new URI(dataURI);
+ if(!"data".equals(uri.getScheme())) {
+ throw new IllegalArgumentException("Invalid Data URI format");
+ }
+ String[] parts = uri.getSchemeSpecificPart().split(",", 2);
+ if (parts.length != 2) {
+ throw new IllegalArgumentException("Invalid Data URI format");
+ }
+
+ String metadata = parts[0];
+ String dataPart = parts[1];
+
+ // Extract media type and encoding
+ String[] metadataParts = metadata.split(";");
+ var mediaType = metadataParts[0];
+ var encoding = metadataParts.length > 1
+ ? metadataParts[metadataParts.length - 1].equals("base64")
+ ? Encoding.BASE64
+ : Encoding.NONE
+ : Encoding.NONE;
+
+ var parameters = IntStream.range(1, metadataParts.length - (encoding
== Encoding.BASE64 ? 2 : 1))
+ .mapToObj(i->metadataParts[i])
+ .toList();
+
+ return new DataUri(mediaType, parameters, encoding,
decodeData(encoding, dataPart));
+ }
+
+ // canonical constructor
+ @SneakyThrows
+ public DataUri(
+ @Nullable String mediaType,
+ @Nullable List<String> parameters,
+ @Nullable Encoding encoding,
+ @Nullable byte[] data) {
+ this.mediaType = StringUtils.hasLength(mediaType) ? mediaType :
"text/plain;charset=US-ASCII";
+ this.parameters = parameters!=null ? List.copyOf(parameters) :
List.of();
+ this.encoding = encoding!=null ? encoding : Encoding.NONE;
+ this.data = data!=null ? data : new byte[0];
+ // validate
+ new URI(toExternalForm());
+ }
+
+ @Override
+ public String toString() {
+ return toExternalForm();
+ }
+
+ /**
+ * Constructs a string representation of this {@code DataUri}, that
+ * can be parsed via {@link DataUri#parse(String)}.
+ */
+ public String toExternalForm() {
+ var sb = new StringBuilder("data:")
+ .append(mediaType);
+ if(!parameters.isEmpty()) {
+ parameters.forEach(param->sb.append(";").append(param));
+ }
+ if (encoding == Encoding.BASE64) {
+ sb.append(";base64");
+ }
+ sb.append(",").append(encodeData());
+ return sb.toString();
+ }
+
+ /**
+ * Equality by value.
+ * @implNote override needed, otherwise the data array would be compared
by reference
+ */
+ @Override
+ public boolean equals(Object o) {
+ if(this == o) return true;
+ return o instanceof DataUri other
+ ? Objects.equals(this.mediaType, other.mediaType)
+ && Objects.equals(this.encoding, other.encoding)
+ && Arrays.equals(this.data, other.data)
+ : false;
+ }
+
+ // -- HELPER
+
+ private String encodeData() {
+ return encoding == Encoding.BASE64
+ ? Base64.getEncoder().encodeToString(data)
+ : URLEncoder.encode(new String(data, StandardCharsets.UTF_8),
StandardCharsets.UTF_8).replace("+", "%20");
+ }
+
+ private static byte[] decodeData(Encoding encoding, String dataPart) {
+ return encoding == Encoding.BASE64
+ ? Base64.getDecoder().decode(dataPart)
+ : dataPart.getBytes(StandardCharsets.UTF_8);
+ }
+
+}
+
diff --git
a/api/applib/src/test/java/org/apache/causeway/applib/value/DataUriTest.java
b/api/applib/src/test/java/org/apache/causeway/applib/value/DataUriTest.java
new file mode 100644
index 00000000000..7e0f966968e
--- /dev/null
+++ b/api/applib/src/test/java/org/apache/causeway/applib/value/DataUriTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.causeway.applib.value;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import lombok.RequiredArgsConstructor;
+
+class DataUriTest {
+
+ @RequiredArgsConstructor
+ enum Scenario {
+ HALLO_WORLD("text/plain", null, DataUri.Encoding.BASE64, "Hello,
World!".getBytes(StandardCharsets.UTF_8),
+ "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="),
+
+// TEXT_OTHER("text/vnd-example+xyz;foo=bar", "base64",
"R0lGODdh".getBytes(StandardCharsets.UTF_8),
+// "data:text/vnd-example+xyz;foo=bar;base64,R0lGODdh"),
+//
+ TEXT_ADVANCED("text/plain", List.of("charset=UTF-8", "page=21"),
DataUri.Encoding.NONE, "the data:1234,5678".getBytes(StandardCharsets.UTF_8),
+
"data:text/plain;charset=UTF-8;page=21,the%20data%3A1234%2C5678"),
+ ;
+ final String mediaType;
+ final List<String> parameters;
+ final DataUri.Encoding encoding;
+ final byte[] data;
+ final String externalForm;
+ }
+
+ @ParameterizedTest
+ @EnumSource(Scenario.class)
+ void rundtrip(Scenario scenario) {
+ var ref = new DataUri(scenario.mediaType, scenario.parameters,
scenario.encoding, scenario.data);
+ var parsed = DataUri.parse(scenario.externalForm);
+ switch (scenario) {
+ case TEXT_ADVANCED: {
+ assertEquals(scenario.externalForm, ref.toExternalForm());
+ assertEquals(ref, parsed); // check equality relation
+ }
+ default:
+ assertEquals(scenario.externalForm, ref.toExternalForm());
+ assertEquals(ref, parsed); // check equality relation
+ }
+
+ }
+
+}