You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2021/06/24 07:00:17 UTC

[isis] branch master updated: ISIS-2761: fixes Vaadin and JavaFX provisioning issues

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/master by this push:
     new 3e11ab8  ISIS-2761: fixes Vaadin and JavaFX provisioning issues
3e11ab8 is described below

commit 3e11ab8a79a3e10776cca9229497a2e8e3d0b182
Author: Andi Huber <ah...@apache.org>
AuthorDate: Thu Jun 24 09:00:07 2021 +0200

    ISIS-2761: fixes Vaadin and JavaFX provisioning issues
    
    so can work on a shared object-icon resolver utility
    
    also changes the RO Viewer's object-icon resource href value 'image' ->
    'object-icon'
---
 .../session/InteractionServiceDefault.java         |  10 +-
 .../java/demoapp/webapp/vaadin/DemoAppVaadin.java  |  10 +-
 .../javafx/ui/components/object/ObjectViewFx.java  |  27 +++--
 .../viewer/javafx/ui/main/MainViewFx.java          |   8 +-
 .../restfulobjects/applib/RepresentationType.java  |   8 +-
 .../domainobjects/DomainObjectReprRenderer.java    |   6 +-
 .../resources/DomainObjectResourceServerside.java  | 121 +++++----------------
 .../viewer/resources/_DomainObjectIcons.java       |  74 +++++++++++++
 8 files changed, 143 insertions(+), 121 deletions(-)

diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/session/InteractionServiceDefault.java
index 0a3ba9a..5c18284 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/session/InteractionServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/session/InteractionServiceDefault.java
@@ -88,7 +88,7 @@ import lombok.extern.log4j.Log4j2;
 public class InteractionServiceDefault
 implements
     InteractionService,
-        InteractionLayerTracker {
+    InteractionLayerTracker {
 
     @Inject AuthenticationManager authenticationManager;
     @Inject MetamodelEventService runtimeEventService;
@@ -110,7 +110,7 @@ implements
 
     //@PostConstruct .. too early, needs services to be provisioned first
     @EventListener
-    public void init(ContextRefreshedEvent event) {
+    public void init(final ContextRefreshedEvent event) {
 
         requires(authenticationManager, "authenticationManager");
 
@@ -311,7 +311,7 @@ implements
     	return interactionLayerStack.get().size()==1;
     }
 
-    private void postInteractionOpened(IsisInteraction interaction) {
+    private void postInteractionOpened(final IsisInteraction interaction) {
         interactionId.set(interaction.getInteractionId());
         interactionScopeAwareBeans.forEach(bean->bean.beforeEnteringTransactionalBoundary(interaction));
         txBoundaryHandler.onOpen(interaction);
@@ -320,7 +320,7 @@ implements
         interactionScopeLifecycleHandler.onTopLevelInteractionOpened();
     }
 
-    private void preInteractionClosed(IsisInteraction interaction) {
+    private void preInteractionClosed(final IsisInteraction interaction) {
         completeAndPublishCurrentCommand();
         interactionScopeLifecycleHandler.onTopLevelInteractionClosing(); // cleanup the isis-session scope
         val isSynchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
@@ -330,7 +330,7 @@ implements
         interaction.close(); // do this last
     }
 
-    private void closeInteractionLayerStackDownToStackSize(int downToStackSize) {
+    private void closeInteractionLayerStackDownToStackSize(final int downToStackSize) {
 
         log.debug("about to close authenication stack down to size {} (conversation-id={}, total-sessions-on-stack={}, {})",
                 downToStackSize,
diff --git a/examples/demo/vaadin/src/main/java/demoapp/webapp/vaadin/DemoAppVaadin.java b/examples/demo/vaadin/src/main/java/demoapp/webapp/vaadin/DemoAppVaadin.java
index fe86481..00f681d 100644
--- a/examples/demo/vaadin/src/main/java/demoapp/webapp/vaadin/DemoAppVaadin.java
+++ b/examples/demo/vaadin/src/main/java/demoapp/webapp/vaadin/DemoAppVaadin.java
@@ -32,14 +32,16 @@ import org.apache.isis.valuetypes.asciidoc.ui.wkt.IsisModuleValAsciidocUiWkt;
 import org.apache.isis.valuetypes.sse.ui.wkt.IsisModuleValSseUiWkt;
 import org.apache.isis.viewer.wicket.viewer.IsisModuleViewerWicketViewer;
 
-import demoapp.web.DemoAppManifestCommon;
+import demoapp.dom.DemoModuleCommon;
+import demoapp.web.DemoAppManifestJdo;
 
 /**
  * Bootstrap the application.
  */
 @SpringBootApplication
 @Import({
-    DemoAppManifestCommon.class,
+    DemoModuleCommon.class,
+    DemoAppManifestJdo.class,
 
     // INCUBATING
     IsisModuleIncViewerVaadinViewer.class, // vaadin viewer
@@ -59,13 +61,15 @@ public class DemoAppVaadin extends SpringBootServletInitializer {
      * @implNote this is to support the <em>Spring Boot Maven Plugin</em>, which auto-detects an
      * entry point by searching for classes having a {@code main(...)}
      */
-    public static void main(String[] args) {
+    public static void main(final String[] args) {
 
         IsisPresets.logging(InteractionServiceDefault.class, "debug");
 //        IsisPresets.logging(VaadinAuthenticationHandler.class, "debug");
 //        IsisPresets.logging(IsisServletForVaadin.class, "debug");
         IsisPresets.logging(_Probe.class, "debug"); // enable debug entry logging
 
+        System.setProperty("spring.profiles.active", "demo-jdo");
+
         SpringApplication.run(new Class[] { DemoAppVaadin.class }, args);
     }
 
diff --git a/incubator/viewers/javafx/ui/src/main/java/org/apache/isis/incubator/viewer/javafx/ui/components/object/ObjectViewFx.java b/incubator/viewers/javafx/ui/src/main/java/org/apache/isis/incubator/viewer/javafx/ui/components/object/ObjectViewFx.java
index f494419..0c8de07f 100644
--- a/incubator/viewers/javafx/ui/src/main/java/org/apache/isis/incubator/viewer/javafx/ui/components/object/ObjectViewFx.java
+++ b/incubator/viewers/javafx/ui/src/main/java/org/apache/isis/incubator/viewer/javafx/ui/components/object/ObjectViewFx.java
@@ -77,6 +77,11 @@ public class ObjectViewFx extends VBox {
             final Consumer<ManagedAction> actionEventHandler,
             final ManagedObject managedObject) {
 
+        if(ManagedObjects.isNullOrUnspecifiedOrEmpty(managedObject)) {
+            log.warn("invalid managedObject, skipping");
+            return;
+        }
+
         log.info("binding object interaction to owner {}", managedObject.getSpecification().getFeatureIdentifier());
         _Assert.assertTrue(uiContext.getInteractionService().isInInteraction(), "requires an active interaction");
 
@@ -87,20 +92,20 @@ public class ObjectViewFx extends VBox {
         val gridVisitor = new UiGridLayout.Visitor<Pane, TabPane>(this) {
 
             @Override
-            protected void onObjectTitle(Pane container, DomainObjectLayoutData domainObjectData) {
+            protected void onObjectTitle(final Pane container, final DomainObjectLayoutData domainObjectData) {
                 val label = _fx.h2(_fx.newLabel(container, objectTitle));
                 label.maxWidthProperty().bind(
                         container.widthProperty());
             }
 
             @Override
-            protected Pane newRow(Pane container, BS3Row bs3Row) {
+            protected Pane newRow(final Pane container, final BS3Row bs3Row) {
                 val uiRow = _fx.newFlowPane(container);
                 return uiRow;
             }
 
             @Override
-            protected Pane newCol(Pane container, BS3Col bs3col) {
+            protected Pane newCol(final Pane container, final BS3Col bs3col) {
 
                 val uiCol = _fx.newVBox(container);
 
@@ -119,7 +124,7 @@ public class ObjectViewFx extends VBox {
             }
 
             @Override
-            protected Pane newActionPanel(Pane container) {
+            protected Pane newActionPanel(final Pane container) {
                 val uiActionPanel = _fx.newFlowPane(container);
                 _fx.toolbarLayout(uiActionPanel);
 
@@ -127,13 +132,13 @@ public class ObjectViewFx extends VBox {
             }
 
             @Override
-            protected TabPane newTabGroup(Pane container, BS3TabGroup tabGroupData) {
+            protected TabPane newTabGroup(final Pane container, final BS3TabGroup tabGroupData) {
                 val uiTabGroup = _fx.newTabGroup(container);
                 return uiTabGroup;
             }
 
             @Override
-            protected Pane newTab(TabPane container, BS3Tab tabData) {
+            protected Pane newTab(final TabPane container, final BS3Tab tabData) {
                 val uiTab = _fx.newTab(container, tabData.getName());
                 val uiTabContentPane = new VBox();
                 uiTab.setContent(uiTabContentPane);
@@ -141,7 +146,7 @@ public class ObjectViewFx extends VBox {
             }
 
             @Override
-            protected Pane newFieldSet(Pane container, FieldSet fieldSetData) {
+            protected Pane newFieldSet(final Pane container, final FieldSet fieldSetData) {
 
                 val titledPanel = _fx.add(container, new TitledPanel(fieldSetData.getName()));
 
@@ -156,12 +161,12 @@ public class ObjectViewFx extends VBox {
 
 
             @Override
-            protected void onClearfix(Pane container, BS3ClearFix clearFixData) {
+            protected void onClearfix(final Pane container, final BS3ClearFix clearFixData) {
                 // TODO Auto-generated method stub
             }
 
             @Override
-            protected void onAction(Pane container, ActionLayoutData actionData) {
+            protected void onAction(final Pane container, final ActionLayoutData actionData) {
 
                 val owner = managedObject;
                 val interaction = ActionInteraction.start(owner, actionData.getId(), Where.OBJECT_FORMS);
@@ -188,7 +193,7 @@ public class ObjectViewFx extends VBox {
             }
 
             @Override
-            protected void onProperty(Pane container, PropertyLayoutData propertyData) {
+            protected void onProperty(final Pane container, final PropertyLayoutData propertyData) {
 
                 val owner = managedObject;
 
@@ -222,7 +227,7 @@ public class ObjectViewFx extends VBox {
             }
 
             @Override
-            protected void onCollection(Pane container, CollectionLayoutData collectionData) {
+            protected void onCollection(final Pane container, final CollectionLayoutData collectionData) {
 
                 val owner = managedObject;
 
diff --git a/incubator/viewers/javafx/ui/src/main/java/org/apache/isis/incubator/viewer/javafx/ui/main/MainViewFx.java b/incubator/viewers/javafx/ui/src/main/java/org/apache/isis/incubator/viewer/javafx/ui/main/MainViewFx.java
index 7ec0abf..4f0f228 100644
--- a/incubator/viewers/javafx/ui/src/main/java/org/apache/isis/incubator/viewer/javafx/ui/main/MainViewFx.java
+++ b/incubator/viewers/javafx/ui/src/main/java/org/apache/isis/incubator/viewer/javafx/ui/main/MainViewFx.java
@@ -26,6 +26,7 @@ import org.apache.isis.applib.annotation.Where;
 import org.apache.isis.applib.services.iactnlayer.InteractionService;
 import org.apache.isis.core.metamodel.context.MetaModelContext;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
+import org.apache.isis.core.metamodel.spec.ManagedObjects;
 import org.apache.isis.core.runtime.context.IsisAppCommonContext;
 import org.apache.isis.incubator.viewer.javafx.model.context.UiContextFx;
 import org.apache.isis.incubator.viewer.javafx.model.events.JavaFxViewerConfig;
@@ -112,7 +113,7 @@ public class MainViewFx {
         header.getTertiary().buildMenuItems(commonContext, rightMenuBuilder);
     }
 
-    private void replaceContent(Node node) {
+    private void replaceContent(final Node node) {
         contentPane.getChildren().clear();
         contentPane.getChildren().add(node);
     }
@@ -122,8 +123,9 @@ public class MainViewFx {
         uiContext.route(metaModelContext::getHomePageAdapter);
     }
 
-    private Node uiComponentForActionResult(ManagedObject actionResult) {
-        if (actionResult.getSpecification().isParentedOrFreeCollection()) {
+    private Node uiComponentForActionResult(final ManagedObject actionResult) {
+        if (ManagedObjects.isSpecified(actionResult)
+                && actionResult.getSpecification().isParentedOrFreeCollection()) {
             return TableViewFx.fromCollection(uiContext, actionResult, Where.STANDALONE_TABLES);
         } else {
             return ObjectViewFx.fromObject(
diff --git a/viewers/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/RepresentationType.java b/viewers/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/RepresentationType.java
index 7dd4858..93ba153 100644
--- a/viewers/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/RepresentationType.java
+++ b/viewers/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/RepresentationType.java
@@ -108,7 +108,7 @@ public enum RepresentationType {
             RestfulMediaType.APPLICATION_JSON_OBJECT_LAYOUT_BS3,
             RestfulMediaType.APPLICATION_XML_OBJECT_LAYOUT_BS3,
             null),
-    OBJECT_IMAGE(
+    OBJECT_ICON(
             "image/png",
             "image/png",
             null),
@@ -198,16 +198,16 @@ public enum RepresentationType {
      * Clones the (immutable) {@link #getMediaType() media type}, adding in one additional
      * parameter value.
      */
-    public MediaType getMediaType(String parameter, String paramValue) {
+    public MediaType getMediaType(final String parameter, final String paramValue) {
         return getJsonMediaType(Collections.singletonMap(parameter, paramValue));
     }
 
-    public MediaType getJsonMediaType(Map<String, String> mediaTypeParams) {
+    public MediaType getJsonMediaType(final Map<String, String> mediaTypeParams) {
         Map<String, String> parameters = new HashMap<>(jsonMediaType.getParameters());
         parameters.putAll(mediaTypeParams);
         return new MediaType(jsonMediaType.getType(), jsonMediaType.getSubtype(), parameters);
     }
-    public MediaType getXmlMediaType(Map<String, String> mediaTypeParams) {
+    public MediaType getXmlMediaType(final Map<String, String> mediaTypeParams) {
         if(xmlMediaType == null) {
             return null;
         }
diff --git a/viewers/restfulobjects/rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/domainobjects/DomainObjectReprRenderer.java b/viewers/restfulobjects/rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/domainobjects/DomainObjectReprRenderer.java
index 3a6a0ee..2d869d4 100644
--- a/viewers/restfulobjects/rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/domainobjects/DomainObjectReprRenderer.java
+++ b/viewers/restfulobjects/rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/domainobjects/DomainObjectReprRenderer.java
@@ -81,8 +81,8 @@ public class DomainObjectReprRenderer extends ReprRendererAbstract<DomainObjectR
 
         final Rel rel = Rel.OBJECT_ICON;
         final String objectRef = ManagedObjects.stringifyElseFail(objectAdapter, "/");
-        final String url = "objects/" + objectRef + "/image";
-        return LinkBuilder.newBuilder(resourceContext, rel.getName(), RepresentationType.OBJECT_IMAGE, url);
+        final String url = "objects/" + objectRef + "/object-icon";
+        return LinkBuilder.newBuilder(resourceContext, rel.getName(), RepresentationType.OBJECT_ICON, url);
     }
 
     private static enum Mode {
@@ -457,7 +457,7 @@ public class DomainObjectReprRenderer extends ReprRendererAbstract<DomainObjectR
     }
 
 
-    public static Object valueOrRef(IResourceContext context, JsonValueEncoder jsonValueEncoder, ManagedObject adapter) {
+    public static Object valueOrRef(final IResourceContext context, final JsonValueEncoder jsonValueEncoder, final ManagedObject adapter) {
 
         val spec = adapter.getSpecification();
         if(spec.isValue()) {
diff --git a/viewers/restfulobjects/viewer/src/main/java/org/apache/isis/viewer/restfulobjects/viewer/resources/DomainObjectResourceServerside.java b/viewers/restfulobjects/viewer/src/main/java/org/apache/isis/viewer/restfulobjects/viewer/resources/DomainObjectResourceServerside.java
index 39aada8..26d8fc8 100644
--- a/viewers/restfulobjects/viewer/src/main/java/org/apache/isis/viewer/restfulobjects/viewer/resources/DomainObjectResourceServerside.java
+++ b/viewers/restfulobjects/viewer/src/main/java/org/apache/isis/viewer/restfulobjects/viewer/resources/DomainObjectResourceServerside.java
@@ -18,7 +18,6 @@
  */
 package org.apache.isis.viewer.restfulobjects.viewer.resources;
 
-import java.io.IOException;
 import java.io.InputStream;
 import java.util.Optional;
 
@@ -45,12 +44,9 @@ import org.apache.isis.applib.layout.component.DomainObjectLayoutData;
 import org.apache.isis.applib.layout.component.PropertyLayoutData;
 import org.apache.isis.applib.layout.grid.Grid;
 import org.apache.isis.applib.layout.links.Link;
-import org.apache.isis.commons.internal.base._Bytes;
-import org.apache.isis.commons.internal.base._Strings;
+import org.apache.isis.applib.services.iactnlayer.InteractionLayerTracker;
 import org.apache.isis.commons.internal.codec._UrlDecoderUtil;
-import org.apache.isis.commons.internal.resources._Resources;
 import org.apache.isis.core.config.IsisConfiguration;
-import org.apache.isis.applib.services.iactnlayer.InteractionLayerTracker;
 import org.apache.isis.core.metamodel.consent.Consent;
 import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
 import org.apache.isis.core.metamodel.context.MetaModelContext;
@@ -60,7 +56,6 @@ import org.apache.isis.core.metamodel.interactions.managed.MemberInteraction.Acc
 import org.apache.isis.core.metamodel.interactions.managed.PropertyInteraction;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
 import org.apache.isis.core.metamodel.spec.ManagedObjects.EntityUtil;
-import org.apache.isis.core.metamodel.spec.ObjectSpecification;
 import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
 import org.apache.isis.viewer.restfulobjects.applib.Rel;
 import org.apache.isis.viewer.restfulobjects.applib.RepresentationType;
@@ -102,7 +97,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT, RestfulMediaType.APPLICATION_JSON_ERROR,
         MediaType.APPLICATION_XML, RestfulMediaType.APPLICATION_XML_OBJECT, RestfulMediaType.APPLICATION_XML_ERROR
     })
-    public Response persist(@PathParam("domainType") String domainType, final InputStream object) {
+    public Response persist(@PathParam("domainType") final String domainType, final InputStream object) {
 
         val resourceContext = createResourceContext(
                 RepresentationType.DOMAIN_OBJECT, Where.OBJECT_FORMS, RepresentationService.Intent.JUST_CREATED);
@@ -158,7 +153,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT, RestfulMediaType.APPLICATION_JSON_ERROR,
         MediaType.APPLICATION_XML, RestfulMediaType.APPLICATION_XML_OBJECT, RestfulMediaType.APPLICATION_XML_ERROR
     })
-    public Response object(@PathParam("domainType") String domainType, @PathParam("instanceId") final String instanceId) {
+    public Response object(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId) {
 
         val resourceContext = createResourceContext(
                 RepresentationType.DOMAIN_OBJECT, Where.OBJECT_FORMS, RepresentationService.Intent.ALREADY_PERSISTENT);
@@ -178,7 +173,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT, RestfulMediaType.APPLICATION_JSON_ERROR,
         MediaType.APPLICATION_XML, RestfulMediaType.APPLICATION_XML_OBJECT, RestfulMediaType.APPLICATION_XML_ERROR
     })
-    public Response object(@PathParam("domainType") String domainType, @PathParam("instanceId") final String instanceId, final InputStream object) {
+    public Response object(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, final InputStream object) {
 
         val resourceContext = createResourceContext(
                 RepresentationType.DOMAIN_OBJECT, Where.OBJECT_FORMS, RepresentationService.Intent.ALREADY_PERSISTENT);
@@ -209,14 +204,14 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
     @DELETE
     @Path("/{domainType}/{instanceId}")
     @Override
-    public Response deleteMethodNotSupported(@PathParam("domainType") String domainType, @PathParam("instanceId") String instanceId) {
+    public Response deleteMethodNotSupported(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId) {
         throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.METHOD_NOT_ALLOWED, "Deleting objects is not supported.");
     }
 
     @POST
     @Path("/{domainType}/{instanceId}")
     @Override
-    public Response postMethodNotAllowed(@PathParam("domainType") String domainType, @PathParam("instanceId") String instanceId) {
+    public Response postMethodNotAllowed(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId) {
         throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.METHOD_NOT_ALLOWED, "Posting to object resource is not allowed.");
     }
 
@@ -225,64 +220,9 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
     // domain object layout
     // //////////////////////////////////////////////////////////
 
-    static class IconKey {
-        private final Class<?> domainClass;
-        private final String modifier;
-
-        IconKey(final Class<?> domainClass, final String modifier) {
-            this.domainClass = domainClass;
-            this.modifier = modifier;
-        }
-
-        String getImageName() {
-            final StringBuilder buf = new StringBuilder(domainClass.getSimpleName());
-            if(!_Strings.isNullOrEmpty(modifier)) {
-                buf.append("-").append(modifier);
-            }
-            buf.append(".png");
-            return buf.toString();
-        }
-
-        @Override
-        public boolean equals(final Object o) {
-            if (this == o)
-                return true;
-            if (o == null || getClass() != o.getClass())
-                return false;
-
-            final IconKey iconKey = (IconKey) o;
-
-            if (domainClass != null ? !domainClass.equals(iconKey.domainClass) : iconKey.domainClass != null) {
-                return false;
-            }
-            return modifier != null ? modifier.equals(iconKey.modifier) : iconKey.modifier == null;
-        }
-
-        @Override
-        public int hashCode() {
-            int result = domainClass != null ? domainClass.hashCode() : 0;
-            result = 31 * result + (modifier != null ? modifier.hashCode() : 0);
-            return result;
-        }
-
-        byte[] toBytes() {
-            String imageName =  getImageName();
-
-            try {
-
-                final InputStream resource = _Resources.load(domainClass, imageName);
-                return _Bytes.of(resource);
-
-            } catch (IOException e) {
-                return null;
-            }
-
-        }
-    }
-
     @Override
     @GET
-    @Path("/{domainType}/{instanceId}/image")
+    @Path("/{domainType}/{instanceId}/object-icon")
     @Consumes({ MediaType.WILDCARD })
     @Produces({
         "image/png"
@@ -293,18 +233,15 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
             @PathParam("instanceId")
             final String instanceId) {
 
-        createResourceContext(
-                RepresentationType.OBJECT_LAYOUT, Where.ANYWHERE, RepresentationService.Intent.NOT_APPLICABLE);
+//        createResourceContext(
+//                RepresentationType.OBJECT_ICON, Where.ANYWHERE, RepresentationService.Intent.NOT_APPLICABLE);
 
-        final ManagedObject objectAdapter = getObjectAdapterElseThrowNotFound(domainType, instanceId);
-        final ObjectSpecification objectSpec = objectAdapter.getSpecification();
-        final String iconName = objectSpec.getIconName(objectAdapter);
-        final Class<?> correspondingClass = objectSpec.getCorrespondingClass();
-        final IconKey iconKey = new IconKey(correspondingClass, iconName);
-        final byte[] bytes = iconKey.toBytes();
-        return bytes != null
-                ? Response.ok(bytes).build()
-                        : Response.status(Response.Status.NOT_FOUND).build();
+        val objectAdapter = getObjectAdapterElseThrowNotFound(domainType, instanceId);
+
+        return _DomainObjectIcons.loadIcon(objectAdapter)
+            .map(Response::ok)
+            .orElseGet(Responses::ofNotFound)
+            .build();
     }
 
     @Override
@@ -426,7 +363,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_PROPERTY, RestfulMediaType.APPLICATION_JSON_ERROR,
         MediaType.APPLICATION_XML, RestfulMediaType.APPLICATION_XML_OBJECT_PROPERTY, RestfulMediaType.APPLICATION_XML_ERROR
     })
-    public Response propertyDetails(@PathParam("domainType") String domainType, @PathParam("instanceId") final String instanceId, @PathParam("propertyId") final String propertyId) {
+    public Response propertyDetails(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, @PathParam("propertyId") final String propertyId) {
 
         val resourceContext = createResourceContext(
                 RepresentationType.OBJECT_PROPERTY, Where.OBJECT_FORMS, RepresentationService.Intent.NOT_APPLICABLE);
@@ -448,7 +385,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_PROPERTY, RestfulMediaType.APPLICATION_JSON_ERROR,
         MediaType.APPLICATION_XML, RestfulMediaType.APPLICATION_XML_OBJECT_PROPERTY, RestfulMediaType.APPLICATION_XML_ERROR
     })
-    public Response modifyProperty(@PathParam("domainType") String domainType, @PathParam("instanceId") final String instanceId, @PathParam("propertyId") final String propertyId, final InputStream body) {
+    public Response modifyProperty(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, @PathParam("propertyId") final String propertyId, final InputStream body) {
 
         val resourceContext = createResourceContext(
                 ResourceDescriptor.generic(Where.OBJECT_FORMS, RepresentationService.Intent.NOT_APPLICABLE));
@@ -478,7 +415,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_PROPERTY, RestfulMediaType.APPLICATION_JSON_ERROR,
         MediaType.APPLICATION_XML, RestfulMediaType.APPLICATION_XML_OBJECT_PROPERTY, RestfulMediaType.APPLICATION_XML_ERROR
     })
-    public Response clearProperty(@PathParam("domainType") String domainType, @PathParam("instanceId") final String instanceId, @PathParam("propertyId") final String propertyId) {
+    public Response clearProperty(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, @PathParam("propertyId") final String propertyId) {
 
         val resourceContext = createResourceContext(
                 ResourceDescriptor.generic(Where.OBJECT_FORMS, RepresentationService.Intent.NOT_APPLICABLE));
@@ -498,7 +435,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
     @POST
     @Path("/{domainType}/{instanceId}/properties/{propertyId}")
     @Override
-    public Response postPropertyNotAllowed(@PathParam("domainType") String domainType, @PathParam("instanceId") String instanceId, @PathParam("propertyId") String propertyId) {
+    public Response postPropertyNotAllowed(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, @PathParam("propertyId") final String propertyId) {
         throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.METHOD_NOT_ALLOWED, "Posting to a property resource is not allowed.");
     }
 
@@ -514,7 +451,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_COLLECTION, RestfulMediaType.APPLICATION_JSON_ERROR,
         MediaType.APPLICATION_XML, RestfulMediaType.APPLICATION_XML_OBJECT_COLLECTION, RestfulMediaType.APPLICATION_XML_ERROR
     })
-    public Response accessCollection(@PathParam("domainType") String domainType, @PathParam("instanceId") final String instanceId, @PathParam("collectionId") final String collectionId) {
+    public Response accessCollection(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, @PathParam("collectionId") final String collectionId) {
 
         val resourceContext = createResourceContext(
                 RepresentationType.OBJECT_COLLECTION, Where.PARENTED_TABLES, RepresentationService.Intent.NOT_APPLICABLE);
@@ -534,7 +471,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_COLLECTION, RestfulMediaType.APPLICATION_JSON_ERROR,
         MediaType.APPLICATION_XML, RestfulMediaType.APPLICATION_XML_OBJECT_COLLECTION, RestfulMediaType.APPLICATION_XML_ERROR
     })
-    public Response addToSet(@PathParam("domainType") String domainType, @PathParam("instanceId") final String instanceId, @PathParam("collectionId") final String collectionId, final InputStream body) {
+    public Response addToSet(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, @PathParam("collectionId") final String collectionId, final InputStream body) {
 
         throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.METHOD_NOT_ALLOWED, "The framework no longer supports mutable collections.");
     }
@@ -547,7 +484,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_COLLECTION, RestfulMediaType.APPLICATION_JSON_ERROR,
         MediaType.APPLICATION_XML, RestfulMediaType.APPLICATION_XML_OBJECT_COLLECTION, RestfulMediaType.APPLICATION_XML_ERROR
     })
-    public Response addToList(@PathParam("domainType") String domainType, @PathParam("instanceId") final String instanceId, @PathParam("collectionId") final String collectionId, final InputStream body) {
+    public Response addToList(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, @PathParam("collectionId") final String collectionId, final InputStream body) {
 
         throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.METHOD_NOT_ALLOWED, "The framework no longer supports mutable collections.");
     }
@@ -560,7 +497,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_COLLECTION, RestfulMediaType.APPLICATION_JSON_ERROR,
         MediaType.APPLICATION_XML, RestfulMediaType.APPLICATION_XML_OBJECT_COLLECTION, RestfulMediaType.APPLICATION_XML_ERROR
     })
-    public Response removeFromCollection(@PathParam("domainType") String domainType, @PathParam("instanceId") final String instanceId, @PathParam("collectionId") final String collectionId) {
+    public Response removeFromCollection(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, @PathParam("collectionId") final String collectionId) {
 
         throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.METHOD_NOT_ALLOWED, "The framework no longer supports mutable collections.");
     }
@@ -577,7 +514,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_ACTION, RestfulMediaType.APPLICATION_JSON_ERROR,
         MediaType.APPLICATION_XML, RestfulMediaType.APPLICATION_XML_OBJECT_ACTION, RestfulMediaType.APPLICATION_XML_ERROR
     })
-    public Response actionPrompt(@PathParam("domainType") String domainType, @PathParam("instanceId") final String instanceId, @PathParam("actionId") final String actionId) {
+    public Response actionPrompt(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, @PathParam("actionId") final String actionId) {
 
         val resourceContext = createResourceContext(
                 RepresentationType.OBJECT_ACTION, Where.OBJECT_FORMS, RepresentationService.Intent.NOT_APPLICABLE);
@@ -591,21 +528,21 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
     @DELETE
     @Path("/{domainType}/{instanceId}/actions/{actionId}")
     @Override
-    public Response deleteActionPromptNotAllowed(@PathParam("domainType") String domainType, @PathParam("instanceId") String instanceId, @PathParam("actionId") String actionId) {
+    public Response deleteActionPromptNotAllowed(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, @PathParam("actionId") final String actionId) {
         throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.METHOD_NOT_ALLOWED, "Deleting action prompt resource is not allowed.");
     }
 
     @POST
     @Path("/{domainType}/{instanceId}/actions/{actionId}")
     @Override
-    public Response postActionPromptNotAllowed(@PathParam("domainType") String domainType, @PathParam("instanceId") String instanceId, @PathParam("actionId") String actionId) {
+    public Response postActionPromptNotAllowed(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, @PathParam("actionId") final String actionId) {
         throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.METHOD_NOT_ALLOWED, "Posting to an action prompt resource is not allowed.");
     }
 
     @PUT
     @Path("/{domainType}/{instanceId}/actions/{actionId}")
     @Override
-    public Response putActionPromptNotAllowed(@PathParam("domainType") String domainType, @PathParam("instanceId") String instanceId, @PathParam("actionId") String actionId) {
+    public Response putActionPromptNotAllowed(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, @PathParam("actionId") final String actionId) {
         throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.METHOD_NOT_ALLOWED, "Putting to an action prompt resource is not allowed.");
     }
 
@@ -676,7 +613,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         MediaType.APPLICATION_XML, RestfulMediaType.APPLICATION_XML_ACTION_RESULT, RestfulMediaType.APPLICATION_XML_ERROR
     })
     public Response invokeAction(
-            @PathParam("domainType") String domainType,
+            @PathParam("domainType") final String domainType,
             @PathParam("instanceId") final String instanceId,
             @PathParam("actionId") final String actionId,
             final InputStream body) {
@@ -696,7 +633,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
     @DELETE
     @Path("/{domainType}/{instanceId}/actions/{actionId}/invoke")
     @Override
-    public Response deleteInvokeActionNotAllowed(@PathParam("domainType") String domainType, @PathParam("instanceId") String instanceId, @PathParam("actionId") String actionId) {
+    public Response deleteInvokeActionNotAllowed(@PathParam("domainType") final String domainType, @PathParam("instanceId") final String instanceId, @PathParam("actionId") final String actionId) {
         throw RestfulObjectsApplicationException.createWithMessage(RestfulResponse.HttpStatusCode.METHOD_NOT_ALLOWED, "Deleting an action invocation resource is not allowed.");
     }
 
diff --git a/viewers/restfulobjects/viewer/src/main/java/org/apache/isis/viewer/restfulobjects/viewer/resources/_DomainObjectIcons.java b/viewers/restfulobjects/viewer/src/main/java/org/apache/isis/viewer/restfulobjects/viewer/resources/_DomainObjectIcons.java
new file mode 100644
index 0000000..1efafef
--- /dev/null
+++ b/viewers/restfulobjects/viewer/src/main/java/org/apache/isis/viewer/restfulobjects/viewer/resources/_DomainObjectIcons.java
@@ -0,0 +1,74 @@
+/*
+ *  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.isis.viewer.restfulobjects.viewer.resources;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+
+import org.apache.isis.commons.internal.base._Bytes;
+import org.apache.isis.commons.internal.base._Strings;
+import org.apache.isis.commons.internal.resources._Resources;
+import org.apache.isis.core.metamodel.spec.ManagedObject;
+import org.apache.isis.core.metamodel.spec.ManagedObjects;
+
+import lombok.val;
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+class _DomainObjectIcons {
+
+    public Optional<byte[]> loadIcon(
+            final @Nullable ManagedObject object) {
+
+        if(ManagedObjects.isNullOrUnspecifiedOrEmpty(object)) {
+            return Optional.empty();
+        }
+
+        val spec = object.getSpecification();
+
+        return Optional.ofNullable(
+                loadIcon(
+                        spec.getCorrespondingClass(),
+                        spec.getIconName(object)
+                        ));
+    }
+
+    //TODO[2761] that's a naive implementation; refactor the Wicket Viewer's icon resolving logic into a reusable utility or service
+    private byte[] loadIcon(
+            final Class<?> domainClass,
+            final String iconNameModifier) {
+
+        val iconResourceName = _Strings.isNotEmpty(iconNameModifier)
+                ? domainClass.getSimpleName() + "-" + iconNameModifier
+                : domainClass.getSimpleName();
+
+        try {
+            final InputStream resource = _Resources
+                    .load(domainClass, iconResourceName + ".png");
+            return _Bytes.of(resource);
+
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+}