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 0e8d2221fed CAUSEWAY-3889: adds support for Domain Object Icon Image 
Embedding
0e8d2221fed is described below

commit 0e8d2221fede798e0361039412c1e4c811cd4620
Author: a.huber <[email protected]>
AuthorDate: Mon Aug 18 12:03:55 2025 +0200

    CAUSEWAY-3889: adds support for Domain Object Icon Image Embedding
---
 .../commons/internal/resources/_Resources.java     | 20 ++++-
 .../org/apache/causeway/commons/net/DataUri.java   |  3 +-
 .../core/metamodel/commons/ClassExtensions.java    |  4 +-
 .../metamodel/facets/object/icon/ObjectIcon.java   | 52 ++++---------
 .../facets/object/icon/ObjectIconEmbedded.java     | 31 ++++----
 .../{ObjectIcon.java => ObjectIconUrlBased.java}   | 62 +++++-----------
 .../icons/ObjectIconServiceDefault.java            | 23 ++++--
 .../DemoFixture_extending_ExcelFixture2.java       |  2 +-
 .../DemoToDoItem_create_usingExcelFixture.java     | 10 +--
 .../ExcelModuleDemoUploadService_IntegTest.java    |  2 +-
 .../resources/DomainObjectResourceServerside.java  |  4 +-
 .../wicket/model/models/BookmarkTreeNode.java      | 86 +++++-----------------
 .../wicket/model/models/ImageResourceCache.java    | 12 +--
 .../viewer/wicket/model/models/UiObjectWkt.java    | 29 ++++++--
 .../bookmarkedpages/BookmarkedPagesPanel.java      | 18 +++--
 .../object/icontitle/ObjectIconAndTitlePanel.java  | 13 +++-
 .../apache/causeway/viewer/wicket/ui/util/Wkt.java | 17 +++++
 .../services/ImageResourceCacheClassPath.java      | 17 +++--
 18 files changed, 189 insertions(+), 216 deletions(-)

diff --git 
a/commons/src/main/java/org/apache/causeway/commons/internal/resources/_Resources.java
 
b/commons/src/main/java/org/apache/causeway/commons/internal/resources/_Resources.java
index 5de2fe28bd7..d98218ca3f6 100644
--- 
a/commons/src/main/java/org/apache/causeway/commons/internal/resources/_Resources.java
+++ 
b/commons/src/main/java/org/apache/causeway/commons/internal/resources/_Resources.java
@@ -27,14 +27,15 @@
 import java.util.function.Predicate;
 import java.util.regex.Pattern;
 
+import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
+import org.apache.causeway.commons.functional.Try;
 import org.apache.causeway.commons.internal.base._Bytes;
 import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.commons.internal.context._Context;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 
-import org.jspecify.annotations.NonNull;
 import lombok.SneakyThrows;
 
 /**
@@ -141,7 +142,7 @@ public static String loadAsStringUtf8ElseFail(
      * @param resourceName
      * @return The resource location as an URL, or null if the resource could 
not be found.
      */
-    public static @Nullable URL getResourceUrl(
+    public static Optional<URL> lookupResourceUrl(
             final @NonNull Class<?> contextClass,
             final @NonNull String resourceName) {
 
@@ -149,8 +150,19 @@ public static String loadAsStringUtf8ElseFail(
 
         return Optional
                 .ofNullable(contextClass.getResource(absoluteResourceName))
-                .orElseGet(()->_Context.getDefaultClassLoader()
-                        .getResource(absoluteResourceName));
+                .or(()->Optional
+                        .ofNullable(_Context.getDefaultClassLoader()
+                                .getResource(absoluteResourceName)));
+    }
+
+    @SneakyThrows
+    public static Optional<URI> lookupResourceUri(
+            final @NonNull Class<?> contextClass,
+            final @NonNull String resourceName) {
+        return lookupResourceUrl(contextClass, resourceName)
+            .map(url->Try
+                    .call(url::toURI)
+                    .valueAsNonNullElseFail());
     }
 
     // -- LOCAL vs EXTERNAL resource path
diff --git a/commons/src/main/java/org/apache/causeway/commons/net/DataUri.java 
b/commons/src/main/java/org/apache/causeway/commons/net/DataUri.java
index 6199cda8e2f..164550043a9 100644
--- a/commons/src/main/java/org/apache/causeway/commons/net/DataUri.java
+++ b/commons/src/main/java/org/apache/causeway/commons/net/DataUri.java
@@ -18,6 +18,7 @@
  */
 package org.apache.causeway.commons.net;
 
+import java.io.Serializable;
 import java.net.URI;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
@@ -44,7 +45,7 @@ public record DataUri(
         String mediaType,
         List<String> parameters,
         Encoding encoding,
-        byte[] data) {
+        byte[] data) implements Serializable {
 
     public enum Encoding {
         NONE,
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/commons/ClassExtensions.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/commons/ClassExtensions.java
index 66cd7883623..163a92ac622 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/commons/ClassExtensions.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/commons/ClassExtensions.java
@@ -22,7 +22,6 @@
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
-import java.net.URL;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -121,8 +120,7 @@ public static Method getMethodElseNull(final Class<?> 
clazz, final String method
     }
 
     public static boolean exists(final Class<?> cls, final String 
resourceName) {
-        final URL url = _Resources.getResourceUrl(cls, resourceName);
-        return url != null;
+        return _Resources.lookupResourceUrl(cls, resourceName).isPresent();
     }
 
     static Class<?> asWrapped(final Class<?> primitiveClassExtendee) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/icon/ObjectIcon.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/icon/ObjectIcon.java
index 09656132270..e062125270e 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/icon/ObjectIcon.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/icon/ObjectIcon.java
@@ -18,30 +18,21 @@
  */
 package org.apache.causeway.core.metamodel.facets.object.icon;
 
-import java.io.InputStream;
 import java.io.Serializable;
 import java.net.URL;
-import java.util.Objects;
 
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
-import org.apache.causeway.commons.internal._Constants;
-import org.apache.causeway.commons.internal.base._Bytes;
 import org.apache.causeway.commons.internal.base._StableValue;
-import org.apache.causeway.commons.internal.base._Strings;
+import org.apache.causeway.commons.net.DataUri;
 
 /**
  * Icon image data class-path resource reference.
  *
  * @see ObjectIconService
- * @since 2.0
+ * @since 2.0 revised for 4.0
  */
-public record ObjectIcon(
-        String shortName,
-        URL url,
-        CommonMimeType mimeType,
-        String identifier,
-        _StableValue<byte[]> iconData
-        ) implements Serializable {
+public sealed interface ObjectIcon extends Serializable
+permits ObjectIconEmbedded, ObjectIconUrlBased {
 
     // -- FACTORIES
 
@@ -53,9 +44,8 @@ public static ObjectIcon eager(
             final String shortName,
             final URL url,
             final CommonMimeType mimeType) {
-        var id = _Strings.base64UrlEncode(url.getPath());
-        var objectIcon = new ObjectIcon(shortName, url, mimeType, id, new 
_StableValue<>());
-        objectIcon.asBytes(); // memoize
+        var objectIcon = lazy(shortName, url, mimeType);
+        ((ObjectIconUrlBased) objectIcon).iconData(); // memoize
         return objectIcon;
     }
 
@@ -67,31 +57,17 @@ public static ObjectIcon lazy(
             final String shortName,
             final URL url,
             final CommonMimeType mimeType) {
-        var id = _Strings.base64UrlEncode(url.getPath());
-        return new ObjectIcon(shortName, url, mimeType, id, new 
_StableValue<>());
+        return new ObjectIconUrlBased(shortName, url, mimeType, new 
_StableValue<>());
     }
 
-    // -- EQUALITY
-
-    @Override
-    public final boolean equals(Object o) {
-        if(this == o) return true;
-        return o instanceof ObjectIcon other
-            ? Objects.equals(this.shortName, other.shortName)
-                    && Objects.equals(this.url, other.url)
-                    && Objects.equals(this.mimeType, other.mimeType)
-                    && Objects.equals(this.identifier, other.identifier)
-            : false;
+    public static ObjectIcon embedded(String shortName, DataUri dataUri) {
+        return new ObjectIconEmbedded(shortName, dataUri);
     }
 
-    public byte[] asBytes() {
-        return iconData.orElseSet(()->{
-            try(final InputStream is = url.openStream()){
-                return _Bytes.of(is);
-            } catch (Exception e) {
-                return _Constants.emptyBytes;
-            }
-        });
-    }
+    // --
+
+    String shortName();
+    String mediaType();
+    byte[] iconData();
 
 }
diff --git 
a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ImageResourceCache.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/icon/ObjectIconEmbedded.java
similarity index 53%
copy from 
viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ImageResourceCache.java
copy to 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/icon/ObjectIconEmbedded.java
index a36d27a1c12..29f44cb2070 100644
--- 
a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ImageResourceCache.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/icon/ObjectIconEmbedded.java
@@ -16,24 +16,29 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.viewer.wicket.model.models;
+package org.apache.causeway.core.metamodel.facets.object.icon;
 
-import java.io.Serializable;
-
-import org.apache.wicket.request.resource.ResourceReference;
-
-import org.apache.causeway.core.metamodel.facets.object.icon.ObjectIcon;
+import org.apache.causeway.commons.net.DataUri;
 
 /**
- * Ideally I'd like to move this to the 
<tt>org.apache.causeway.viewer.wicket.model.causeway</tt>
- * package, however to do so would break existing API (gmap3 has a dependency 
on this, for example).
+ * Icon image based on {@link DataUri}
+ *
+ * @see ObjectIconService
+ * @since 4.0
  */
-public interface ImageResourceCache extends Serializable {
+public record ObjectIconEmbedded(
+        String shortName,
+        DataUri dataUri
+        ) implements ObjectIcon {
 
-    //ResourceReference resourceReferenceFor(ManagedObject adapter);
+    @Override
+    public String mediaType() {
+        return dataUri.mediaType();
+    }
 
-    //ResourceReference resourceReferenceForSpec(ObjectSpecification 
objectSpecification);
+    @Override
+    public byte[] iconData() {
+        return dataUri.data();
+    }
 
-    ResourceReference resourceReferenceForObjectIcon(final ObjectIcon 
objectIcon);
-    
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/icon/ObjectIcon.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/icon/ObjectIconUrlBased.java
similarity index 58%
copy from 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/icon/ObjectIcon.java
copy to 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/icon/ObjectIconUrlBased.java
index 09656132270..1c31a4893f4 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/icon/ObjectIcon.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/icon/ObjectIconUrlBased.java
@@ -19,7 +19,6 @@
 package org.apache.causeway.core.metamodel.facets.object.icon;
 
 import java.io.InputStream;
-import java.io.Serializable;
 import java.net.URL;
 import java.util.Objects;
 
@@ -35,40 +34,30 @@
  * @see ObjectIconService
  * @since 2.0
  */
-public record ObjectIcon(
+public record ObjectIconUrlBased(
         String shortName,
         URL url,
         CommonMimeType mimeType,
-        String identifier,
-        _StableValue<byte[]> iconData
-        ) implements Serializable {
+        _StableValue<byte[]> iconDataRef
+        ) implements ObjectIcon {
 
-    // -- FACTORIES
+    public String cacheId() {
+        return _Strings.base64UrlEncode(url.getPath());
+    }
 
-    /**
-     * Create an ObjectIcon and eagerly read in image data from
-     * class-path resources.
-     */
-    public static ObjectIcon eager(
-            final String shortName,
-            final URL url,
-            final CommonMimeType mimeType) {
-        var id = _Strings.base64UrlEncode(url.getPath());
-        var objectIcon = new ObjectIcon(shortName, url, mimeType, id, new 
_StableValue<>());
-        objectIcon.asBytes(); // memoize
-        return objectIcon;
+    public byte[] iconData() {
+        return iconDataRef.orElseSet(()->{
+            try(final InputStream is = url.openStream()){
+                return _Bytes.of(is);
+            } catch (Exception e) {
+                return _Constants.emptyBytes;
+            }
+        });
     }
 
-    /**
-     * Create an ObjectIcon and not yet read in image data from
-     * class-path resources.
-     */
-    public static ObjectIcon lazy(
-            final String shortName,
-            final URL url,
-            final CommonMimeType mimeType) {
-        var id = _Strings.base64UrlEncode(url.getPath());
-        return new ObjectIcon(shortName, url, mimeType, id, new 
_StableValue<>());
+    @Override
+    public String mediaType() {
+        return mimeType.mimeType().getBaseType();
     }
 
     // -- EQUALITY
@@ -76,22 +65,11 @@ public static ObjectIcon lazy(
     @Override
     public final boolean equals(Object o) {
         if(this == o) return true;
-        return o instanceof ObjectIcon other
+        return o instanceof ObjectIconUrlBased other
             ? Objects.equals(this.shortName, other.shortName)
-                    && Objects.equals(this.url, other.url)
-                    && Objects.equals(this.mimeType, other.mimeType)
-                    && Objects.equals(this.identifier, other.identifier)
+                && Objects.equals(this.url, other.url)
+                && Objects.equals(this.mimeType, other.mimeType)
             : false;
     }
 
-    public byte[] asBytes() {
-        return iconData.orElseSet(()->{
-            try(final InputStream is = url.openStream()){
-                return _Bytes.of(is);
-            } catch (Exception e) {
-                return _Constants.emptyBytes;
-            }
-        });
-    }
-
 }
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/icons/ObjectIconServiceDefault.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/icons/ObjectIconServiceDefault.java
index 720abe71b3e..5b1b3c040c6 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/icons/ObjectIconServiceDefault.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/icons/ObjectIconServiceDefault.java
@@ -33,6 +33,7 @@
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.core.io.ResourceLoader;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
@@ -41,6 +42,7 @@
 import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.commons.internal.resources._Resources;
+import org.apache.causeway.commons.net.DataUri;
 import org.apache.causeway.core.metamodel.facets.object.icon.ObjectIcon;
 import org.apache.causeway.core.metamodel.facets.object.icon.ObjectIconService;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
@@ -82,9 +84,16 @@ public ObjectIcon getObjectIcon(
             final @Nullable String iconNameModifier) {
 
         var domainClass = spec.getCorrespondingClass();
-        var iconResourceKey = _Strings.isNotEmpty(iconNameModifier)
-                ? domainClass.getName() + "-" + iconNameModifier
-                : domainClass.getName();
+
+        var suffix = "";
+        if(StringUtils.hasLength(iconNameModifier)) {
+            suffix = "-" + iconNameModifier;
+            if(iconNameModifier.startsWith("data:")) {
+                return ObjectIcon.embedded(domainClass.getSimpleName(), 
DataUri.parse(iconNameModifier));
+            }
+        }
+
+        var iconResourceKey = domainClass.getName() + suffix;
 
         // also memoize unsuccessful icon lookups (as fallback), so we don't 
search repeatedly
 
@@ -105,9 +114,10 @@ public ObjectIcon getObjectIcon(
     private ObjectIcon getObjectFallbackIcon() {
         return fallbackIcon.orElseSet(()->ObjectIcon.eager(
                 "ObjectIconFallback",
-                _Resources.getResourceUrl(
+                _Resources.lookupResourceUrl(
                         ObjectIconServiceDefault.class,
-                        "ObjectIconFallback.png"),
+                        "ObjectIconFallback.png")
+                .orElse(null),
                 CommonMimeType.PNG));
     }
 
@@ -195,8 +205,7 @@ private static Optional<URL> classPathResource(
             throw _Exceptions
                 .illegalArgument("invalid relative resourceName %s", 
relativeResourceName);
         }
-        var resourceUrl = _Resources.getResourceUrl(contextClass, 
relativeResourceName);
-        return Optional.ofNullable(resourceUrl);
+        return _Resources.lookupResourceUrl(contextClass, 
relativeResourceName);
     }
 
 }
diff --git 
a/extensions/core/excel/fixture/src/main/java/org/apache/causeway/extensions/excel/fixtures/demoapp/demomodule/fixturescripts/DemoFixture_extending_ExcelFixture2.java
 
b/extensions/core/excel/fixture/src/main/java/org/apache/causeway/extensions/excel/fixtures/demoapp/demomodule/fixturescripts/DemoFixture_extending_ExcelFixture2.java
index cbbba40e658..93bf85f8097 100644
--- 
a/extensions/core/excel/fixture/src/main/java/org/apache/causeway/extensions/excel/fixtures/demoapp/demomodule/fixturescripts/DemoFixture_extending_ExcelFixture2.java
+++ 
b/extensions/core/excel/fixture/src/main/java/org/apache/causeway/extensions/excel/fixtures/demoapp/demomodule/fixturescripts/DemoFixture_extending_ExcelFixture2.java
@@ -47,7 +47,7 @@ public DemoFixture_extending_ExcelFixture2(){
     @Override
     protected void execute(final ExecutionContext executionContext) {
 
-        setExcelResource(_Resources.getResourceUrl(getClass(), 
getResourceName()));
+        setExcelResource(_Resources.lookupResourceUrl(getClass(), 
getResourceName()).orElse(null));
 
         setMatcher(sheetName -> {
 
diff --git 
a/extensions/core/excel/fixture/src/main/java/org/apache/causeway/extensions/excel/fixtures/demoapp/demomodule/fixturescripts/DemoToDoItem_create_usingExcelFixture.java
 
b/extensions/core/excel/fixture/src/main/java/org/apache/causeway/extensions/excel/fixtures/demoapp/demomodule/fixturescripts/DemoToDoItem_create_usingExcelFixture.java
index 297ec945e58..d9aac500f91 100644
--- 
a/extensions/core/excel/fixture/src/main/java/org/apache/causeway/extensions/excel/fixtures/demoapp/demomodule/fixturescripts/DemoToDoItem_create_usingExcelFixture.java
+++ 
b/extensions/core/excel/fixture/src/main/java/org/apache/causeway/extensions/excel/fixtures/demoapp/demomodule/fixturescripts/DemoToDoItem_create_usingExcelFixture.java
@@ -42,7 +42,7 @@ public DemoToDoItem_create_usingExcelFixture() {
         this(null);
     }
 
-    public DemoToDoItem_create_usingExcelFixture(String ownedBy) {
+    public DemoToDoItem_create_usingExcelFixture(final String ownedBy) {
         this.user = ownedBy;
     }
 
@@ -50,7 +50,7 @@ public DemoToDoItem_create_usingExcelFixture(String ownedBy) {
     private List<ExcelDemoToDoItem> todoItems = _Lists.newArrayList();
 
     @Override
-    public void execute(ExecutionContext executionContext) {
+    public void execute(final ExecutionContext executionContext) {
 
         final String ownedBy = this.user != null ? this.user : 
userService.currentUserNameElseNobody();
 
@@ -59,7 +59,7 @@ public void execute(ExecutionContext executionContext) {
         transactionService.flushTransaction();
     }
 
-    private void installFor(String user, ExecutionContext ec) {
+    private void installFor(final String user, final ExecutionContext ec) {
 
         ec.setParameter("user", user);
 
@@ -73,12 +73,12 @@ private List<ExcelDemoToDoItem> load(
             final ExecutionContext executionContext,
             final String resourceName) {
 
-        final URL excelResource = _Resources.getResourceUrl(getClass(), 
resourceName);
+        final URL excelResource = _Resources.lookupResourceUrl(getClass(), 
resourceName).orElse(null);
         final ExcelFixture excelFixture = new ExcelFixture(excelResource, 
DemoToDoItemRowHandler.class);
         excelFixture.setExcelResourceName(resourceName);
         executionContext.executeChild(this, excelFixture);
 
-        return (List<ExcelDemoToDoItem>) excelFixture.getObjects();
+        return excelFixture.getObjects();
     }
 
     @Inject UserService userService;
diff --git 
a/extensions/core/excel/integtests/src/test/java/org/apache/causeway/extensions/excel/integtests/tests/ExcelModuleDemoUploadService_IntegTest.java
 
b/extensions/core/excel/integtests/src/test/java/org/apache/causeway/extensions/excel/integtests/tests/ExcelModuleDemoUploadService_IntegTest.java
index 994b02538dd..06517532f70 100644
--- 
a/extensions/core/excel/integtests/src/test/java/org/apache/causeway/extensions/excel/integtests/tests/ExcelModuleDemoUploadService_IntegTest.java
+++ 
b/extensions/core/excel/integtests/src/test/java/org/apache/causeway/extensions/excel/integtests/tests/ExcelModuleDemoUploadService_IntegTest.java
@@ -52,7 +52,7 @@ public void setUpData() throws Exception {
     public void uploadSpreadsheet() throws Exception{
 
         // Given
-        final URL excelResource = _Resources.getResourceUrl(getClass(), 
"ToDoItemsWithMultipleSheets.xlsx");
+        final URL excelResource = _Resources.lookupResourceUrl(getClass(), 
"ToDoItemsWithMultipleSheets.xlsx").orElse(null);
         final Blob blob = new ExcelFileBlobConverter().toBlob("unused", 
excelResource);
 
         // When
diff --git 
a/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/DomainObjectResourceServerside.java
 
b/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/DomainObjectResourceServerside.java
index 2b5a1600e1d..615c30f048f 100644
--- 
a/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/DomainObjectResourceServerside.java
+++ 
b/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/DomainObjectResourceServerside.java
@@ -222,8 +222,8 @@ public ResponseEntity<Object> image(
 
         return _EndpointLogging.response(log, "GET 
/objects/{}/{}/object-icon", domainType, instanceId,
             responseFactory.ok(
-                        objectIcon.asBytes(),
-                        
MediaType.parseMediaType(objectIcon.mimeType().baseType())));
+                        objectIcon.iconData(),
+                        MediaType.parseMediaType(objectIcon.mediaType())));
     }
 
     @Override
diff --git 
a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/BookmarkTreeNode.java
 
b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/BookmarkTreeNode.java
index 9453bdb4a99..52fb4a6696f 100644
--- 
a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/BookmarkTreeNode.java
+++ 
b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/BookmarkTreeNode.java
@@ -22,19 +22,20 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.atomic.LongAdder;
+import java.util.function.Consumer;
 
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.ResourceReference;
+import org.jspecify.annotations.NonNull;
 
 import org.apache.causeway.applib.fa.FontAwesomeLayers;
 import org.apache.causeway.applib.services.bookmark.Bookmark;
-import org.apache.causeway.commons.functional.Either;
 import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.collections._Lists;
 import org.apache.causeway.commons.internal.functions._Functions;
+import 
org.apache.causeway.core.metamodel.facets.object.icon.ObjectIconEmbedded;
 
 import lombok.Getter;
-import org.jspecify.annotations.NonNull;
 
 public class BookmarkTreeNode
 implements
@@ -49,12 +50,9 @@ public class BookmarkTreeNode
 
     @Getter private String title;
 
-    /** its either a iconResourceReference or a iconFaClass or neither 
(decomposed for easy serialization) */
     private ResourceReference iconResourceReference;
-    /** its either a iconResourceReference or a FontAwesomeLayers or neither 
(decomposed for easy serialization) */
     private FontAwesomeLayers faLayers;
-
-    //private final Set<Bookmark> propertyBookmarks; ... in support of parents 
referencing their child
+    private ObjectIconEmbedded embedded;
 
     // -- FACTORIES
 
@@ -73,30 +71,27 @@ private BookmarkTreeNode(
 
         this.pageParameters = 
bookmarkableModel.getPageParametersWithoutUiHints();
         this.bookmark = bookmark;
-//        this.propertyBookmarks = bookmarkableModel.streamPropertyBookmarks()
-//                .collect(Collectors.toCollection(HashSet::new));
-
         this.title = bookmarkableModel.getTitle();
 
         _Casts.castTo(UiObjectWkt.class, bookmarkableModel)
-        .map(UiObjectWkt::getIconAsResourceReference)
-        .ifPresent(either->either.accept(
-                iconResourceReference->
-                    this.iconResourceReference = iconResourceReference,
-                faLayers->
-                    this.faLayers = faLayers
-                )
-        );
+        .ifPresent(x->x.visitIconVariantOrElse(
+                rref->{this.iconResourceReference = rref;},
+                embedded->{this.embedded = embedded;},
+                faLayers->{this.faLayers = faLayers;},
+                ()->{}));
 
         this.depth = depth;
     }
 
-    // -- ICON
-
-    public Either<ResourceReference, FontAwesomeLayers> eitherIconOrFaClass() {
-        return faLayers==null
-                ? Either.left(iconResourceReference)
-                : Either.right(faLayers);
+    public void visitIconVariantOrElse(
+            Consumer<ResourceReference> a,
+            Consumer<ObjectIconEmbedded> b,
+            Consumer<FontAwesomeLayers> c,
+            Runnable onNoMatch) {
+        if(this.iconResourceReference!=null) a.accept(iconResourceReference);
+        else if(this.embedded!=null) b.accept(embedded);
+        else if(this.faLayers!=null) c.accept(faLayers);
+        else onNoMatch.run();
     }
 
     // -- COMPARATOR
@@ -184,37 +179,6 @@ private boolean matchAndUpdateTitleFor(final UiObjectWkt 
candidateEntityModel) {
         return inGraph;
     }
 
-//    /**
-//     * Whether or not the provided {@link ActionModelImpl} matches that 
contained
-//     * within this node (taking into account the action's arguments).
-//     *
-//     * If it does match, then the matched node's title is updated to that of 
the provided
-//     * {@link ActionModelImpl}.
-//     * <p>
-//     *
-//     * @return - whether the provided candidate is found or was added to 
this node's tree.
-//     */
-//    private boolean matchFor(final ActionModelImpl candidateActionModel) {
-//
-//        var candidateBookmark = 
candidateActionModel.toBookmark().orElse(null);
-//
-//        // check if target object of the action is the same
-//        if(!Objects.equals(getBookmark(), candidateBookmark)) {
-//            return false;
-//        }
-//
-//        // check if args same
-//        List<String> thisArgs = 
PageParameterNames.ACTION_ARGS.getListFrom(pageParameters);
-//        PageParameters candidatePageParameters = 
candidateActionModel.getPageParameters();
-//        List<String> candidateArgs = 
PageParameterNames.ACTION_ARGS.getListFrom(candidatePageParameters);
-//        if(!Objects.equals(thisArgs, candidateArgs)) {
-//            return false;
-//        }
-//
-//        // ok, a match
-//        return true;
-//    }
-
     /**
      * For given candidate model look into its properties and see whether one 
matches this node's bookmark.
      * If so, we found a parent/child relation for the tree to populate
@@ -231,19 +195,7 @@ private boolean addToGraphIfParented(final 
BookmarkableModel candidateBookmarkab
             }
         });
 
-        if(addedCount.longValue()>0L) {
-            return true;
-        }
-
-//        /* also check the other way around, that is,
-//         * whether the child is referenced from one of the parent's 
properties
-//         */
-//        if(candidateBookmarkableModel.toBookmark()
-//                .map(propertyBookmarks::contains)
-//                .orElse(false)) {
-//            return this.addChild(candidateBookmarkableModel).isPresent();
-//        }
-        return false;
+        return addedCount.longValue()>0L;
     }
 
 }
diff --git 
a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ImageResourceCache.java
 
b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ImageResourceCache.java
index a36d27a1c12..7562baf2441 100644
--- 
a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ImageResourceCache.java
+++ 
b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ImageResourceCache.java
@@ -22,18 +22,10 @@
 
 import org.apache.wicket.request.resource.ResourceReference;
 
-import org.apache.causeway.core.metamodel.facets.object.icon.ObjectIcon;
+import 
org.apache.causeway.core.metamodel.facets.object.icon.ObjectIconUrlBased;
 
-/**
- * Ideally I'd like to move this to the 
<tt>org.apache.causeway.viewer.wicket.model.causeway</tt>
- * package, however to do so would break existing API (gmap3 has a dependency 
on this, for example).
- */
 public interface ImageResourceCache extends Serializable {
 
-    //ResourceReference resourceReferenceFor(ManagedObject adapter);
-
-    //ResourceReference resourceReferenceForSpec(ObjectSpecification 
objectSpecification);
+    ResourceReference resourceReferenceForObjectIcon(final ObjectIconUrlBased 
objectIcon);
 
-    ResourceReference resourceReferenceForObjectIcon(final ObjectIcon 
objectIcon);
-    
 }
diff --git 
a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/UiObjectWkt.java
 
b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/UiObjectWkt.java
index 4dc97c318ba..f60aca4186e 100644
--- 
a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/UiObjectWkt.java
+++ 
b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/UiObjectWkt.java
@@ -21,12 +21,13 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
+import java.util.function.Consumer;
 import java.util.stream.Stream;
 
 import org.apache.wicket.Component;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.ResourceReference;
-
+import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.Identifier;
@@ -40,6 +41,8 @@
 import org.apache.causeway.core.metamodel.commons.ViewOrEditMode;
 import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
 import org.apache.causeway.core.metamodel.facets.object.icon.ObjectIcon;
+import 
org.apache.causeway.core.metamodel.facets.object.icon.ObjectIconEmbedded;
+import 
org.apache.causeway.core.metamodel.facets.object.icon.ObjectIconUrlBased;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
@@ -55,7 +58,6 @@
 import org.apache.causeway.viewer.wicket.model.util.PageParameterUtils;
 
 import lombok.Getter;
-import org.jspecify.annotations.NonNull;
 import lombok.Setter;
 import lombok.Synchronized;
 import lombok.extern.slf4j.Slf4j;
@@ -188,10 +190,25 @@ public Either<ObjectIcon, FontAwesomeLayers> getIcon() {
         return getManagedObject().eitherIconOrFaLayers();
     }
 
-    public Either<ResourceReference, FontAwesomeLayers> 
getIconAsResourceReference() {
-        return getIcon()
-                .mapLeft(objectIcon->
-                    
imageResourceCache().resourceReferenceForObjectIcon(objectIcon));
+    public void visitIconVariantOrElse(
+            Consumer<ResourceReference> a,
+            Consumer<ObjectIconEmbedded> b,
+            Consumer<FontAwesomeLayers> c,
+            Runnable onNoMatch) {
+        getIcon().accept(
+            objectIcon->{
+                if(objectIcon instanceof ObjectIconUrlBased urlBased){
+                    var rref = 
imageResourceCache().resourceReferenceForObjectIcon(urlBased);
+                    if(rref!=null) {
+                        a.accept(rref);
+                    } else {
+                        onNoMatch.run();
+                    }
+                } else if(objectIcon instanceof ObjectIconEmbedded embedded){
+                    b.accept(embedded);
+                }
+            },
+            fontAwesomeLayers->c.accept(fontAwesomeLayers));
     }
 
     @Override
diff --git 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/bookmarkedpages/BookmarkedPagesPanel.java
 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/bookmarkedpages/BookmarkedPagesPanel.java
index 01a75b71cac..6a06a14e5e1 100644
--- 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/bookmarkedpages/BookmarkedPagesPanel.java
+++ 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/bookmarkedpages/BookmarkedPagesPanel.java
@@ -19,7 +19,6 @@
 package org.apache.causeway.viewer.wicket.ui.components.bookmarkedpages;
 
 import java.util.List;
-import java.util.Optional;
 
 import jakarta.inject.Inject;
 
@@ -35,6 +34,7 @@
 import org.apache.wicket.util.string.Strings;
 
 import 
org.apache.causeway.applib.exceptions.unrecoverable.ObjectNotFoundException;
+import org.apache.causeway.applib.value.Markup;
 import org.apache.causeway.viewer.wicket.model.models.BookmarkTreeNode;
 import org.apache.causeway.viewer.wicket.model.models.BookmarkedPagesModel;
 import org.apache.causeway.viewer.wicket.model.models.PageType;
@@ -138,17 +138,23 @@ public void renderHead(final IHeaderResponse response) {
                                 bookmarkNode.getPageParameters(),
                                 
pageClassRegistry.getPageClass(PageType.DOMAIN_OBJECT)));
 
-                bookmarkNode.eitherIconOrFaClass()
-                .accept(
+                bookmarkNode.visitIconVariantOrElse(
                         iconResourceRef->{
-                            Optional.ofNullable(iconResourceRef)
-                            .ifPresent(icon->
-                                Wkt.imageAddCachable(link, 
ID_BOOKMARKED_PAGE_ICON, icon));
+                            Wkt.imageAddCachable(link, 
ID_BOOKMARKED_PAGE_ICON, iconResourceRef);
+                            WktComponents.permanentlyHide(link, 
ID_BOOKMARKED_PAGE_ICON_FA);
+                        },
+                        embedded->{
+                            Wkt.markupAdd(link, ID_BOOKMARKED_PAGE_ICON, 
Markup.embeddedImage(embedded.dataUri()).html());
                             WktComponents.permanentlyHide(link, 
ID_BOOKMARKED_PAGE_ICON_FA);
                         },
                         faLayers->{
                             WktComponents.permanentlyHide(link, 
ID_BOOKMARKED_PAGE_ICON);
                             Wkt.faIconLayersAdd(link, 
ID_BOOKMARKED_PAGE_ICON_FA, faLayers);
+                        },
+                        ()->{
+                            // on no match: hide all
+                            WktComponents.permanentlyHide(link, 
ID_BOOKMARKED_PAGE_ICON_FA);
+                            WktComponents.permanentlyHide(link, 
ID_BOOKMARKED_PAGE_ICON);
                         });
 
                 Wkt.labelAdd(link, ID_BOOKMARKED_PAGE_TITLE, 
bookmarkNode.getTitle());
diff --git 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/object/icontitle/ObjectIconAndTitlePanel.java
 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/object/icontitle/ObjectIconAndTitlePanel.java
index 295dd510b5f..d8021f913c6 100644
--- 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/object/icontitle/ObjectIconAndTitlePanel.java
+++ 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/object/icontitle/ObjectIconAndTitlePanel.java
@@ -28,6 +28,8 @@
 import org.apache.causeway.commons.internal.assertions._Assert;
 import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.commons.internal.base._Text;
+import 
org.apache.causeway.core.metamodel.facets.object.icon.ObjectIconEmbedded;
+import 
org.apache.causeway.core.metamodel.facets.object.icon.ObjectIconUrlBased;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
 import org.apache.causeway.core.metamodel.object.MmTitleUtils;
@@ -112,8 +114,15 @@ private AbstractLink createLinkWithIconAndTitle() {
             linkedDomainObject.eitherIconOrFaLayers()
             .accept(
                     objectIcon->{
-                        Wkt.imageAddCachable(link, ID_OBJECT_ICON,
-                                
getImageResourceCache().resourceReferenceForObjectIcon(objectIcon));
+                        if(objectIcon instanceof ObjectIconEmbedded 
iconEmbedded) {
+                            Wkt.imageAddEmbedded(link, ID_OBJECT_ICON, 
iconEmbedded.dataUri());
+                        } else if(objectIcon instanceof ObjectIconUrlBased 
iconUrlBased) {
+                            Wkt.imageAddCachable(link, ID_OBJECT_ICON,
+                                    
getImageResourceCache().resourceReferenceForObjectIcon(iconUrlBased));
+                        } else {
+                            throw new IllegalArgumentException("Unexpected 
value: " + objectIcon);
+                        }
+
                         WktComponents.permanentlyHide(link, 
ID_OBJECT_FONT_AWESOME_LEFT);
                         WktComponents.permanentlyHide(link, 
ID_OBJECT_FONT_AWESOME_RIGHT);
                     },
diff --git 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/util/Wkt.java
 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/util/Wkt.java
index 699b18d5c43..ddfc45b352b 100644
--- 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/util/Wkt.java
+++ 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/util/Wkt.java
@@ -110,6 +110,7 @@
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import 
org.apache.causeway.commons.internal.functions._Functions.SerializableFunction;
 import 
org.apache.causeway.commons.internal.functions._Functions.SerializableSupplier;
+import org.apache.causeway.commons.net.DataUri;
 import org.apache.causeway.core.config.CausewayConfiguration.Viewer.Wicket;
 import org.apache.causeway.core.metamodel.tabular.DataTableInteractive;
 import org.apache.causeway.viewer.commons.model.components.UiString;
@@ -827,10 +828,26 @@ public Image imageCachable(final String id, final 
ResourceReference imageResourc
         };
     }
 
+    public Image imageEmbedded(final String id, final DataUri dataUri) {
+        return new Image(id, "embedded") {
+            private static final long serialVersionUID = 1L;
+            @Override protected boolean shouldAddAntiCacheParameter() { return 
false; }
+
+            @Override
+            protected String buildSrcAttribute(ComponentTag tag) {
+                return dataUri.toExternalForm();
+            }
+        };
+    }
+
     public Image imageAddCachable(final MarkupContainer container, final 
String id, final ResourceReference imageResource) {
         return add(container, imageCachable(id, imageResource));
     }
 
+    public Image imageAddEmbedded(final MarkupContainer container, final 
String id, final DataUri imageResource) {
+        return add(container, imageEmbedded(id, imageResource));
+    }
+
     // -- LABEL
 
     public Label label(final String id, final String label) {
diff --git 
a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/services/ImageResourceCacheClassPath.java
 
b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/services/ImageResourceCacheClassPath.java
index b21ce1176ff..84d5c09fb8f 100644
--- 
a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/services/ImageResourceCacheClassPath.java
+++ 
b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/services/ImageResourceCacheClassPath.java
@@ -37,6 +37,7 @@
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.core.metamodel.facets.object.icon.ObjectIcon;
+import 
org.apache.causeway.core.metamodel.facets.object.icon.ObjectIconUrlBased;
 import org.apache.causeway.viewer.wicket.model.models.ImageResourceCache;
 import 
org.apache.causeway.viewer.wicket.viewer.CausewayModuleViewerWicketViewer;
 
@@ -54,7 +55,7 @@ public class ImageResourceCacheClassPath
     public static final String LOGICAL_TYPE_NAME =
             CausewayModuleViewerWicketViewer.NAMESPACE + 
".ImageResourceCacheClassPath";
 
-    @Configuration
+    @Configuration(proxyBeanMethods = false)
     public static class AutoConfiguration {
         @Bean
         @Named(LOGICAL_TYPE_NAME)
@@ -67,7 +68,7 @@ public ImageResourceCacheClassPath 
imageResourceCacheClassPath() {
     private static final long serialVersionUID = 1L;
 
     @Override
-    public ResourceReference resourceReferenceForObjectIcon(final ObjectIcon 
objectIcon) {
+    public ResourceReference resourceReferenceForObjectIcon(final 
ObjectIconUrlBased objectIcon) {
         return new ObjectIconResourceReference(objectIcon);
     }
 
@@ -80,8 +81,8 @@ private static class ObjectIconResourceReference
 
         private final @NonNull ObjectIconResource objectIconResource;
 
-        public ObjectIconResourceReference(final ObjectIcon objectIcon) {
-            super(new Key(Application.class.getName(), 
objectIcon.identifier(), null, null, null));
+        public ObjectIconResourceReference(final ObjectIconUrlBased 
objectIcon) {
+            super(new Key(Application.class.getName(), objectIcon.cacheId(), 
null, null, null));
             this.objectIconResource = new ObjectIconResource(objectIcon);
         }
 
@@ -103,14 +104,14 @@ private static class ObjectIconResource
         @Override
         protected ResourceResponse newResourceResponse(final Attributes 
attributes) {
 
-            var imageDataBytes = objectIcon.asBytes();
+            var imageDataBytes = objectIcon.iconData();
             final long size = imageDataBytes.length;
-            ResourceResponse resourceResponse = new ResourceResponse();
-            resourceResponse.setContentType(objectIcon.mimeType().baseType());
+            var resourceResponse = new ResourceResponse();
+            resourceResponse.setContentType(objectIcon.mediaType());
             resourceResponse.setAcceptRange(ContentRangeType.BYTES);
             resourceResponse.setContentLength(size);
             resourceResponse.setFileName(objectIcon.shortName());
-            RequestCycle cycle = RequestCycle.get();
+            var cycle = RequestCycle.get();
             Long startbyte = cycle.getMetaData(CONTENT_RANGE_STARTBYTE);
             Long endbyte = cycle.getMetaData(CONTENT_RANGE_ENDBYTE);
             resourceResponse.setWriteCallback(


Reply via email to