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

ahuber pushed a commit to branch 3937-grid.api.overhaul
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/3937-grid.api.overhaul by this 
push:
     new 9746304b624 CAUSEWAY-3937: loading and caching are orthogonal concerns
9746304b624 is described below

commit 9746304b62486f19fb779f2fe94c0f0e3242c8d6
Author: Andi Huber <[email protected]>
AuthorDate: Tue Oct 28 15:28:09 2025 +0100

    CAUSEWAY-3937: loading and caching are orthogonal concerns
---
 api/applib/src/main/java/module-info.java          |   3 +-
 .../applib/layout/resource}/LayoutResource.java    |  21 +-
 .../layout/resource}/LayoutResourceLoader.java     |  10 +-
 .../causeway/applib/services/grid/GridService.java |  75 +++-----
 .../applib/services/grid/package-info.java         |  26 ---
 .../metamodel/CausewayModuleCoreMetamodel.java     |   4 +-
 .../metamodel/facets/object/grid/BSGridFacet.java  |   9 +-
 .../core/metamodel/services/grid/GridCache.java    | 211 +++++++++------------
 .../core/metamodel/services/grid/GridLoader.java   | 102 ++--------
 .../services/grid/GridLoadingContext.java          |   8 +-
 .../services/grid/GridObjectMemberResolver.java    |  14 +-
 .../services/grid/GridServiceDefault.java          |  88 ++++-----
 .../{GridLoader.java => LayoutResourceLookup.java} |  59 ++++--
 .../grid/spi/LayoutResourceLoaderDefault.java      |   2 +
 .../services/layout/LayoutServiceDefault.java      |   7 +-
 .../metamodel/MetaModelServiceDefault.java         |   3 +-
 .../services/grid/GridCache_resourceNameTest.java  |  48 +----
 .../metamodel/services/grid/GridLoadingTest.java   |  27 +--
 .../services/grid/GridXmlRoundtripTest.java        |   9 +-
 .../spiimpl/LayoutResourceLoaderFromGithub.java    |   4 +-
 .../viewer/controller/ResourceController.java      |   3 +-
 21 files changed, 281 insertions(+), 452 deletions(-)

diff --git a/api/applib/src/main/java/module-info.java 
b/api/applib/src/main/java/module-info.java
index 2cff49c4aa3..9ce9612ccbe 100644
--- a/api/applib/src/main/java/module-info.java
+++ b/api/applib/src/main/java/module-info.java
@@ -35,12 +35,13 @@
     exports org.apache.causeway.applib.graph;
     exports org.apache.causeway.applib.id;
     exports org.apache.causeway.applib.jaxb;
+    exports org.apache.causeway.applib.layout;
     exports org.apache.causeway.applib.layout.component;
     exports org.apache.causeway.applib.layout.grid.bootstrap;
     exports org.apache.causeway.applib.layout.links;
     exports org.apache.causeway.applib.layout.menubars.bootstrap;
     exports org.apache.causeway.applib.layout.menubars;
-    exports org.apache.causeway.applib.layout;
+    exports org.apache.causeway.applib.layout.resource;
     exports org.apache.causeway.applib.locale;
     exports org.apache.causeway.applib.mixins.dto;
     exports org.apache.causeway.applib.mixins.layout;
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResource.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/layout/resource/LayoutResource.java
similarity index 67%
rename from 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResource.java
rename to 
api/applib/src/main/java/org/apache/causeway/applib/layout/resource/LayoutResource.java
index 82c8aff25b7..1db90c16247 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResource.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/layout/resource/LayoutResource.java
@@ -16,15 +16,26 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.core.metamodel.services.grid.spi;
+package org.apache.causeway.applib.layout.resource;
 
-import org.jspecify.annotations.NonNull;
+import java.util.Objects;
 
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
 
+/**
+ * Layout data record with name, format and contents (XML, JSON, etc.) based 
on format.
+ *
+ * @since 4.0 {@index}
+ */
 public record LayoutResource(
-        @NonNull String resourceName,
-        @NonNull CommonMimeType format,
-        @NonNull String content) {
+        String resourceName,
+        CommonMimeType format,
+        String content) {
+
+    public LayoutResource {
+        Objects.requireNonNull(resourceName);
+        Objects.requireNonNull(format);
+        Objects.requireNonNull(content);
+    }
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoader.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/layout/resource/LayoutResourceLoader.java
similarity index 86%
rename from 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoader.java
rename to 
api/applib/src/main/java/org/apache/causeway/applib/layout/resource/LayoutResourceLoader.java
index 7e280f337ba..b2274903130 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoader.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/layout/resource/LayoutResourceLoader.java
@@ -16,7 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.core.metamodel.services.grid.spi;
+package org.apache.causeway.applib.layout.resource;
 
 import java.util.Optional;
 
@@ -27,7 +27,7 @@
 /**
  * SPI for grid loading.
  *
- * @since 2.0 {@index}
+ * @since 4.0 {@index}
  */
 public interface LayoutResourceLoader {
 
@@ -35,7 +35,7 @@ public interface LayoutResourceLoader {
      * Try to locate and load a {@link LayoutResource} by type and name.
      */
     Try<LayoutResource> tryLoadLayoutResource(
-            final @NonNull Class<?> type,
+            final @NonNull Class<?> domainObject,
             final @NonNull String candidateResourceName);
 
     /**
@@ -46,9 +46,9 @@ Try<LayoutResource> tryLoadLayoutResource(
      * <p>Silently ignores exceptions underneath, if any.
      */
     default Optional<LayoutResource> lookupLayoutResource(
-            final @NonNull Class<?> type,
+            final @NonNull Class<?> domainObject,
             final @NonNull String candidateResourceName) {
-        return tryLoadLayoutResource(type, candidateResourceName)
+        return tryLoadLayoutResource(domainObject, candidateResourceName)
                 .getValue();
     }
 
diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridService.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridService.java
index c83e0dc3fd3..b05a53c479e 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridService.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridService.java
@@ -18,23 +18,35 @@
  */
 package org.apache.causeway.applib.services.grid;
 
+import java.util.EnumSet;
+import java.util.Optional;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
 import org.apache.causeway.applib.annotation.ActionLayout;
 import org.apache.causeway.applib.annotation.CollectionLayout;
 import org.apache.causeway.applib.annotation.DomainObjectLayout;
 import org.apache.causeway.applib.annotation.PropertyLayout;
 import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
 import org.apache.causeway.applib.services.layout.LayoutExportStyle;
+import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 
 /**
  * Loads the layout (grid) for any domain class.
- *
- * <p> Acts on top of {@link GridMarshaller} and any {@link 
GridSystemService}(s) registered with Spring.
+ * Also supports various formats {@link LayoutExportStyle} for export.
  *
  * @since 1.x revised for 4.0 {@index}
  */
 public interface GridService {
 
+    public record LayoutKey(
+        @NonNull Class<?> domainClass,
+        /** layout suffix */
+        @Nullable String layoutIfAny) {
+    }
+
     /**
      * Whether dynamic reloading of layouts is enabled.
      *
@@ -47,26 +59,10 @@ public interface GridService {
      *
      * <p> Acts as a no-op if not {@link #supportsReloading()}.
      */
-    void remove(Class<?> domainClass);
-
-    /**
-     * Whether any persisted layout metadata (eg a <code>.layout.xml</code> 
file) exists for this domain class.
-     */
-    boolean existsFor(Class<?> domainClass);
-
-    /**
-     * Returns a new instance of a {@link BSGrid} for the specified domain 
class,
-     * for example as loaded from a <code>layout.xml</code> file.
-     *
-     * <p>If non exists, returns <code>null</code>.  (The caller can then
-     * use {@link GridService#defaultGridFor(Class)} to obtain a
-     * default grid if necessary).
-     *
-     */
-    BSGrid load(final Class<?> domainClass);
+    void invalidate(Class<?> domainClass);
 
     /**
-     * Returns an alternative layout for the domain class.
+     * Returns a normalized grid for the domain class.
      *
      * <p>The alternative layout name can for example be returned by the
      * domain object's <code>layout()</code> method, whereby - based on the
@@ -74,22 +70,8 @@ public interface GridService {
      *
      * <p>The default implementation uses the layout name to search for a 
differently
      * named layout file, <code>[domainClass].layout.[layout].xml</code>.
-     */
-    BSGrid load(Class<?> domainClass, String layout);
-
-    /**
-     * Returns a default grid; eg where none can be loaded using {@link 
#load(Class)}.
      *
-     * <p>Used when no existing grid layout exists for a domain class.
-     *
-     * <p>The default implementation searches through all available
-     * {@link GridSystemService}s and asks each in turn for a
-     * {@link GridSystemService#defaultGrid(Class) default grid}.
-     */
-    BSGrid defaultGridFor(Class<?> domainClass);
-
-    /**
-     * Returns a normalized grid for the domain class obtained previously 
using {@link #load(Class)}.
+     * <p>When no specific grid layout is found returns a generic fallback.
      *
      * <p>If a 'normalized' grid is persisted as the <code>layout.xml</code>, 
then the expectation is that
      * any ordering metadata from layout annotations can be removed from the 
domain class
@@ -100,7 +82,7 @@ public interface GridService {
      * layout XML (in other words moving towards a {@link #complete(BSGrid) 
complete} grid.  Metadata within the
      * <code>layout.xml</code> file takes precedence over any annotations.
      */
-    BSGrid normalize(BSGrid grid);
+    BSGrid loadAndNormalize(LayoutKey layoutKey);
 
     /**
      * Modifies the provided {@link BSGrid} with additional metadata, broadly 
speaking corresponding to the
@@ -119,30 +101,23 @@ public interface GridService {
      * most of the layout annotations ({@link DomainObjectLayout}, {@link 
ActionLayout}, {@link PropertyLayout},
      * {@link CollectionLayout} will still be retained in the domain class 
code.
      *
-     * @param grid
      */
     BSGrid minimal(BSGrid grid);
 
     // -- LAYOUT EXPORT
 
-    GridMarshaller marshaller();
+    EnumSet<CommonMimeType> supportedFormats();
+    Optional<GridMarshaller> marshaller(CommonMimeType format);
 
     default BSGrid toGridForExport(
             final Class<?> domainClass,
             final LayoutExportStyle style) {
 
-        // don't use the grid from the facet, because it will be modified 
subsequently.
-        BSGrid grid = load(domainClass);
-        if(grid == null) {
-            grid = defaultGridFor(domainClass);
-        }
-        grid = normalize(grid); // required so the grid's tns and 
schema-locations get populated
-        if (style == LayoutExportStyle.COMPLETE) {
-            return complete(grid);
-        }
-        if (style == LayoutExportStyle.MINIMAL) {
-            return minimal(grid);
-        }
+        var grid = loadAndNormalize(new LayoutKey(domainClass, null));
+
+        if (style == LayoutExportStyle.COMPLETE) return complete(grid);
+        if (style == LayoutExportStyle.MINIMAL) return minimal(grid);
+
         throw _Exceptions.unmatchedCase(style);
     }
 
diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/grid/package-info.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/grid/package-info.java
deleted file mode 100644
index bccd22a454f..00000000000
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/grid/package-info.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- *  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.
- */
-
-/**
- * The {@link org.apache.causeway.applib.services.grid.GridService} 
encapsulates a single layout grid system which
- * can be used to customize the layout of domain objects.
- *
- *
- */
-package org.apache.causeway.applib.services.grid;
\ No newline at end of file
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
index 7de1f847710..a853ba43960 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
@@ -32,6 +32,7 @@
 
 import org.apache.causeway.applib.CausewayModuleApplib;
 import org.apache.causeway.applib.graph.tree.TreeAdapter;
+import org.apache.causeway.applib.layout.resource.LayoutResourceLoader;
 import org.apache.causeway.applib.services.appfeat.ApplicationFeatureSort;
 import org.apache.causeway.applib.services.grid.GridMarshaller;
 import org.apache.causeway.applib.services.message.MessageService;
@@ -57,9 +58,8 @@
 import 
org.apache.causeway.core.metamodel.services.exceprecog.ExceptionRecognizerForRecoverableException;
 import org.apache.causeway.core.metamodel.services.grid.GridLoadingContext;
 import org.apache.causeway.core.metamodel.services.grid.GridMarshallerXml;
-import org.apache.causeway.core.metamodel.services.grid.GridServiceDefault;
 import 
org.apache.causeway.core.metamodel.services.grid.GridObjectMemberResolver.FallbackLayoutDataSource;
-import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoader;
+import org.apache.causeway.core.metamodel.services.grid.GridServiceDefault;
 import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoaderDefault;
 import 
org.apache.causeway.core.metamodel.services.idstringifier.IdStringifierLookupService;
 import 
org.apache.causeway.core.metamodel.services.inject.ServiceInjectorDefault;
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/grid/BSGridFacet.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/grid/BSGridFacet.java
index e4abfe06f87..61ad5f698fa 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/grid/BSGridFacet.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/grid/BSGridFacet.java
@@ -19,7 +19,6 @@
 package org.apache.causeway.core.metamodel.facets.object.grid;
 
 import java.util.Map;
-import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.BiConsumer;
 
@@ -28,6 +27,7 @@
 
 import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
 import org.apache.causeway.applib.services.grid.GridService;
+import org.apache.causeway.applib.services.grid.GridService.LayoutKey;
 import org.apache.causeway.commons.internal.base._Lazy;
 import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
@@ -114,12 +114,7 @@ private boolean hasLayoutPrefixFacet() {
 
     private BSGrid load(final @NonNull String layoutPrefix) {
         var domainClass = objSpec().getCorrespondingClass();
-        var grid = Optional.ofNullable(
-                // loads from object's XML if available
-                gridService.load(domainClass, 
_Strings.emptyToNull(layoutPrefix)))
-                // loads from default-XML if available
-                .orElseGet(()->gridService.defaultGridFor(domainClass));
-        var bsGrid = gridService.normalize(grid);
+        var bsGrid = gridService.loadAndNormalize(new LayoutKey(domainClass, 
_Strings.emptyToNull(layoutPrefix)));
         return bsGrid;
     }
 
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridCache.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridCache.java
index e064c4e3dc0..2840fc3392b 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridCache.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridCache.java
@@ -18,50 +18,32 @@
  */
 package org.apache.causeway.core.metamodel.services.grid;
 
-import java.util.EnumSet;
-import java.util.HashMap;
 import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-import org.jspecify.annotations.NonNull;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
 
 import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
+import org.apache.causeway.applib.layout.resource.LayoutResource;
 import org.apache.causeway.applib.mixins.metamodel.Object_rebuildMetamodel;
-import org.apache.causeway.applib.services.grid.GridMarshaller;
-import org.apache.causeway.applib.services.message.MessageService;
-import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
-import org.apache.causeway.core.metamodel.services.grid.GridLoader.LayoutKey;
+import org.apache.causeway.applib.services.grid.GridService.LayoutKey;
+
 import lombok.extern.slf4j.Slf4j;
 
 /**
- * Cache for {@link BSGrid} instances,
- * delegating grid loading to the {@link GridLoader}.
+ * Cache for {@link BSGrid} instances,.
  *
  * @since 4.0
  */
 @Slf4j
 record GridCache(
-    GridLoader gridLoader,
-    MessageService messageService,
-    /**
-     * Whether dynamic reloading of layouts is enabled.
-     *
-     * <p> The default implementation enables reloading for prototyping mode,
-     * disables in production
-     */
-    boolean supportsReloading,
-    Map<LayoutKey, BSGrid> gridCache,
+    Map<LayoutKey, BSGrid> gridsByKey,
     // for better logging messages (used only in prototyping mode)
-    Map<LayoutKey, String> badContentByKey) {
+    Map<LayoutKey, LayoutResource> badLayoutResourceByKey) {
 
     public GridCache(
             final GridLoadingContext gridLoadingContext) {
-        this(new GridLoader(gridLoadingContext.layoutResourceLoaders()),
-            gridLoadingContext.messageService(),
-            gridLoadingContext.supportsReloading(),
-            new HashMap<>(), new HashMap<>());
+        this(
+            new ConcurrentHashMap<>(), new ConcurrentHashMap<>());
     }
 
     /**
@@ -71,111 +53,94 @@ public GridCache(
      * <p>This is called by the {@link Object_rebuildMetamodel} mixin action.
      */
     public void remove(final Class<?> domainClass) {
-        if(!supportsReloading()) return;
-
-        final String layoutIfAny = null;
-        var layoutKey = new LayoutKey(domainClass, layoutIfAny);
-        badContentByKey.remove(layoutKey);
-        gridCache.remove(layoutKey);
+        
badLayoutResourceByKey.entrySet().removeIf(entry->entry.getKey().domainClass().equals(domainClass));
+        
gridsByKey.entrySet().removeIf(entry->entry.getKey().domainClass().equals(domainClass));
     }
 
-    /**
-     * Whether any persisted layout metadata (eg a <code>.layout.xml</code> 
file) exists for this domain class.
-     *
-     * <p>If none exists, will return null.
-     */
-    public boolean existsFor(final Class<?> domainClass, final 
EnumSet<CommonMimeType> supportedFormats) {
-        return gridLoader.loadLayoutResource(new LayoutKey(domainClass, null), 
supportedFormats).isPresent();
+    public BSGrid computeIfAbsent(final LayoutKey layoutKey, final 
Function<LayoutKey, BSGrid> factory) {
+        return gridsByKey.computeIfAbsent(layoutKey, factory);
     }
 
     /**
-     * Optionally returns a new instance of a {@link BSGrid},
-     * based on whether the underlying resource could be found, loaded and 
parsed.
-     *
-     * <p>The layout alternative will typically be specified through a
-     * `layout()` method on the domain object, the value of which is used
-     * for the suffix of the layout file (eg "Customer-layout.archived.xml"
-     * to use a different layout for customers that have been archived).
-     *
-     * @throws UnsupportedOperationException - when format is not supported
+     * Stores a normalized, validated grid.
      */
-    public Optional<BSGrid> load(
-            final Class<?> domainClass,
-            final String layoutIfAny,
-            final @NonNull GridMarshaller marshaller) {
-
-        var supportedFormats = marshaller.supportedFormats();
-
-        var layoutKey = new LayoutKey(domainClass, layoutIfAny);
-        var layoutResource = gridLoader.loadLayoutResource(layoutKey, 
supportedFormats).orElse(null);
-        if(layoutResource == null) {
-            log.debug(
-                    "Failed to locate or load layout resource for class {}, "
-                    + "with layout-suffix (if any) {}, "
-                    + "using layout-resource-loaders {}.",
-                    domainClass.getName(), layoutIfAny,
-                    gridLoader().layoutResourceLoaders().stream()
-                        .map(Object::getClass)
-                        .map(Class::getName)
-                        .collect(Collectors.joining(", ")));
-            return Optional.empty();
-        }
-
-        if(supportsReloading()) {
-            final String badContent = badContentByKey.get(layoutKey);
-            if(badContent != null) {
-                if(Objects.equals(layoutResource.content(), badContent)) {
-                    // seen this before and already logged; just quit
-                    return Optional.empty();
-                } else {
-                    // this different content might be good
-                    badContentByKey.remove(layoutKey);
-                }
-            }
-        } else {
-            // if cached, serve from cache - otherwise fall through
-            final BSGrid grid = gridCache.get(layoutKey);
-            if(grid != null) return Optional.of(grid);
-        }
-
-        try {
-            final BSGrid grid = marshaller
-                .unmarshal(domainClass, layoutResource.content(), 
layoutResource.format())
-                .getValue().orElseThrow();
-            if(supportsReloading()) {
-                gridCache.put(layoutKey, grid);
-            }
-            return Optional.of(grid);
-        } catch(Exception ex) {
-
-            if(supportsReloading()) {
-                // save fact that this was bad content, so that we don't log 
again if called next time
-                badContentByKey.put(layoutKey, layoutResource.content());
-            }
-
-            // note that we don't blacklist if the file exists but couldn't be 
parsed;
-            // the developer might fix so we will want to retry.
-            final String resourceName = layoutResource.resourceName();
-            final String message = "Failed to parse " + resourceName + " file 
(" + ex.getMessage() + ")";
-            if(supportsReloading()) {
-                messageService.warnUser(message);
-            }
-            log.warn(message);
-
-            return Optional.empty();
-        }
+    public void putValid(final LayoutKey layoutKey, final BSGrid bsGrid) {
+        gridsByKey.put(layoutKey, bsGrid);
     }
 
     /**
-     * Optionally returns a new instance of a {@link BSGrid},
-     * based on whether the underlying resource could be found, loaded and 
parsed.
-     *
-     * @throws UnsupportedOperationException - when format is not supported
+     * Stores a bad {@link LayoutResource}.
      */
-    public Optional<BSGrid> load(
-            final Class<?> domainClass,
-            final @NonNull GridMarshaller marshaller) {
-        return load(domainClass, null, marshaller);
+    public void putInvalid(final LayoutKey layoutKey, final LayoutResource 
layoutResource) {
+        badLayoutResourceByKey.put(layoutKey, layoutResource);
     }
 
+
+//    /**
+//     * Optionally returns a new instance of a {@link BSGrid},
+//     * based on whether the underlying resource could be found, loaded and 
parsed.
+//     *
+//     * <p>The layout alternative will typically be specified through a
+//     * `layout()` method on the domain object, the value of which is used
+//     * for the suffix of the layout file (eg "Customer-layout.archived.xml"
+//     * to use a different layout for customers that have been archived).
+//     *
+//     * @throws UnsupportedOperationException - when format is not supported
+//     */
+//    public Optional<BSGrid> load(
+//            final LayoutKey layoutKey,
+//            final @NonNull GridMarshaller marshaller) {
+//
+//        var supportedFormats = marshaller.supportedFormats();
+//
+//        var layoutResourceOpt = gridLoader.lookupLayoutResource(layoutKey, 
supportedFormats);
+//        if(layoutResourceOpt.isEmpty()) return Optional.empty();
+//
+//        var layoutResource = layoutResourceOpt.get();
+//
+//        if(supportsReloading()) {
+//            final String badContent = badContentByKey.get(layoutKey);
+//            if(badContent != null) {
+//                if(Objects.equals(layoutResource.content(), badContent)) {
+//                    // seen this before and already logged; just quit
+//                    return Optional.empty();
+//                } else {
+//                    // this different content might be good
+//                    badContentByKey.remove(layoutKey);
+//                }
+//            }
+//        } else {
+//            // if cached, serve from cache - otherwise fall through
+//            final BSGrid grid = gridsByKey.get(layoutKey);
+//            if(grid != null) return Optional.of(grid);
+//        }
+//
+//        try {
+//            final BSGrid grid = marshaller
+//                .unmarshal(domainClass, layoutResource.content(), 
layoutResource.format())
+//                .getValue().orElseThrow();
+//            if(supportsReloading()) {
+//                gridsByKey.put(layoutKey, grid);
+//            }
+//            return Optional.of(grid);
+//        } catch(Exception ex) {
+//
+//            if(supportsReloading()) {
+//                // save fact that this was bad content, so that we don't log 
again if called next time
+//                badContentByKey.put(layoutKey, layoutResource.content());
+//            }
+//
+//            // note that we don't blacklist if the file exists but couldn't 
be parsed;
+//            // the developer might fix so we will want to retry.
+//            final String resourceName = layoutResource.resourceName();
+//            final String message = "Failed to parse " + resourceName + " 
file (" + ex.getMessage() + ")";
+//            if(supportsReloading()) {
+//                messageService.warnUser(message);
+//            }
+//            log.warn(message);
+//
+//            return Optional.empty();
+//        }
+//    }
+
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoader.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoader.java
index 094fe1122ad..5ba50aa9a24 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoader.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoader.java
@@ -18,90 +18,30 @@
  */
 package org.apache.causeway.core.metamodel.services.grid;
 
-import java.util.EnumSet;
-import java.util.Optional;
-import java.util.stream.Stream;
+import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
+import org.apache.causeway.applib.layout.resource.LayoutResource;
+import org.apache.causeway.applib.services.grid.GridService.LayoutKey;
+import org.apache.causeway.commons.functional.Try;
 
-import org.jspecify.annotations.NonNull;
-import org.jspecify.annotations.Nullable;
-
-import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
-import org.apache.causeway.commons.collections.Can;
-import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.reflection._Reflect;
-import 
org.apache.causeway.commons.internal.reflection._Reflect.InterfacePolicy;
-import org.apache.causeway.core.metamodel.services.grid.spi.LayoutResource;
-import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoader;
+import lombok.extern.slf4j.Slf4j;
 
+@Slf4j
 record GridLoader(
-        Can<LayoutResourceLoader> layoutResourceLoaders) {
-
-    public record LayoutKey(
-        @NonNull Class<?> domainClass,
-        /** layout suffix */
-        @Nullable String layoutIfAny) {
-    }
-
-    // -- HELPER
-
-    Optional<LayoutResource> loadLayoutResource(
-            final LayoutKey layoutKey,
-            final EnumSet<CommonMimeType> supportedFormats) {
-        return _Reflect.streamTypeHierarchy(layoutKey.domainClass(), 
InterfacePolicy.EXCLUDE)
-            .flatMap(type->loadContent(type, layoutKey.layoutIfAny(), 
supportedFormats).stream())
-            .findFirst();
+        GridLoadingContext gridLoadingContext) {
+
+    /**
+     * Optionally returns a new instance of a {@link BSGrid},
+     * based on whether the underlying resource could be found, loaded and 
parsed.
+     *
+     * <p>The layout alternative will typically be specified through a
+     * `layout()` method on the domain object, the value of which is used
+     * for the suffix of the layout file (eg "Customer-layout.archived.xml"
+     * to use a different layout for customers that have been archived).
+     */
+    public Try<BSGrid> tryLoad(final LayoutKey layoutKey, final LayoutResource 
layoutResource) {
+        return gridLoadingContext.gridMarshaller(layoutResource.format())
+            .orElseThrow()
+            .unmarshal(layoutKey.domainClass(), layoutResource.content(), 
layoutResource.format());
     }
 
-    private Optional<LayoutResource> loadContent(
-            final @NonNull Class<?> domainClass,
-            final @Nullable String layoutIfAny,
-            final EnumSet<CommonMimeType> supportedFormats) {
-        return streamResourceNameCandidatesFor(domainClass, layoutIfAny, 
supportedFormats)
-            
.flatMap(candidateResourceName->lookupLayoutResourceUsingLoaders(domainClass, 
candidateResourceName).stream())
-            .findFirst();
-    }
-
-    private Stream<String> streamResourceNameCandidatesFor(
-            final @NonNull Class<?> domainClass,
-            final @Nullable String layoutIfAny,
-            final @NonNull  EnumSet<CommonMimeType> supportedFormats) {
-        return supportedFormats.stream()
-                .flatMap(format->streamResourceNameCandidatesFor(domainClass, 
layoutIfAny, format));
-    }
-
-    private Stream<String> streamResourceNameCandidatesFor(
-            final @NonNull Class<?> domainClass,
-            final @Nullable String layoutIfAny,
-            final @NonNull CommonMimeType format) {
-        return format.proposedFileExtensions().stream()
-                
.flatMap(fileExtension->streamResourceNameCandidatesFor(domainClass, 
layoutIfAny, fileExtension));
-    }
-
-    private Stream<String> streamResourceNameCandidatesFor(
-            final @NonNull Class<?> domainClass,
-            final @Nullable String layoutIfAny,
-            final @NonNull String fileExtension) {
-
-        var typeSimpleName = domainClass.getSimpleName();
-
-        return _Strings.isNotEmpty(layoutIfAny)
-                ? Stream.of(
-                        String.format("%s-%s.layout.%s", typeSimpleName, 
layoutIfAny, fileExtension),
-                        String.format("%s.layout.%s", typeSimpleName, 
fileExtension),
-                        String.format("%s.layout.fallback.%s", typeSimpleName, 
fileExtension))
-                : Stream.of(
-                        String.format("%s.layout.%s", typeSimpleName, 
fileExtension),
-                        String.format("%s.layout.fallback.%s", 
typeSimpleName,fileExtension));
-    }
-
-    private Optional<LayoutResource> lookupLayoutResourceUsingLoaders(
-            final @NonNull Class<?> type,
-            final @NonNull String candidateResourceName) {
-
-        return layoutResourceLoaders.stream()
-            .flatMap(loader->loader.lookupLayoutResource(type, 
candidateResourceName).stream())
-            .findFirst();
-    }
-
-
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingContext.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingContext.java
index 8a0d6dd7818..c70f5b69372 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingContext.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingContext.java
@@ -27,6 +27,7 @@
 
 import jakarta.inject.Provider;
 
+import org.apache.causeway.applib.layout.resource.LayoutResourceLoader;
 import org.apache.causeway.applib.services.grid.GridMarshaller;
 import org.apache.causeway.applib.services.message.MessageService;
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
@@ -35,7 +36,6 @@
 import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
 import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
 import 
org.apache.causeway.core.metamodel.services.grid.GridObjectMemberResolver.FallbackLayoutDataSource;
-import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoader;
 import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
 
 /**
@@ -49,6 +49,12 @@ public record GridLoadingContext(
     Map<CommonMimeType, GridMarshaller> marshallersByMime,
     Can<LayoutResourceLoader> layoutResourceLoaders,
     Can<FallbackLayoutDataSource> fallbackLayoutDataSources,
+    /**
+     * Whether dynamic reloading of layouts is enabled.
+     *
+     * <p> The default implementation enables reloading for prototyping mode,
+     * disables in production
+     */
     boolean supportsReloading) {
 
     /** Factory also to be used for JUnit tests. */
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridObjectMemberResolver.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridObjectMemberResolver.java
index 08c6621edb6..dfd97200b68 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridObjectMemberResolver.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridObjectMemberResolver.java
@@ -614,19 +614,21 @@ private void addActionTo(
         actionLayoutData.owner(owner);
     }
 
-    public void normalize(final BSGrid grid, final Class<?> domainClass) {
+    public Optional<BSGrid> normalize(final BSGrid grid, final Class<?> 
domainClass) {
         final boolean valid = validateAndNormalize(grid, domainClass);
         if (valid) {
             overwriteFacets(grid, domainClass);
             if(log.isDebugEnabled()) {
                 log.debug("Grid:\n\n{}\n\n", toXml(grid));
             }
-        } else {
-            if(gridLoadingContext.causewaySystemEnvironment().isPrototyping()) 
{
-                gridLoadingContext.messageService().warnUser("Grid metadata 
errors for " + grid.domainClass().getName() + "; check the error log");
-            }
-            log.error("Grid metadata errors in {}:\n\n{}\n\n", 
grid.domainClass().getName(), toXml(grid));
+            return Optional.of(grid);
+        }
+        if(gridLoadingContext.causewaySystemEnvironment().isPrototyping()) {
+            gridLoadingContext.messageService().warnUser("Grid metadata errors 
for " + grid.domainClass().getName() + "; check the error log");
         }
+        log.error("Grid metadata errors in {}:\n\n{}\n\n", 
grid.domainClass().getName(), toXml(grid));
+        return Optional.empty();
+
     }
 
     /**
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridServiceDefault.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridServiceDefault.java
index 2838a7e510e..fd587507276 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridServiceDefault.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridServiceDefault.java
@@ -18,6 +18,9 @@
  */
 package org.apache.causeway.core.metamodel.services.grid;
 
+import java.util.EnumSet;
+import java.util.Optional;
+
 import jakarta.annotation.Priority;
 import jakarta.inject.Inject;
 import jakarta.inject.Named;
@@ -30,98 +33,79 @@
 import org.apache.causeway.applib.services.grid.GridMarshaller;
 import org.apache.causeway.applib.services.grid.GridService;
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
-import org.apache.causeway.commons.internal.base._Casts;
+import org.apache.causeway.commons.functional.Try;
 import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
 
 /**
  * Default implementation of {@link GridService}.
- *
- * @since 1.x revised for 2.0 {@index}
  */
 @Service
 @Named(CausewayModuleCoreMetamodel.NAMESPACE + ".GridServiceDefault")
 @Priority(PriorityPrecedence.MIDPOINT)
 @Qualifier("Default")
 public record GridServiceDefault(
-    GridLoadingContext gridLoadingContext,
-    GridObjectMemberResolver gridSystemService,
-    GridCache gridCache) implements GridService {
+    GridLoadingContext context,
+    LayoutResourceLookup layoutLookup,
+    GridLoader loader,
+    GridObjectMemberResolver memberResolver,
+    GridCache cache) implements GridService {
 
     @Inject
     public GridServiceDefault(
             final GridLoadingContext gridLoadingContext) {
-        this(gridLoadingContext, new 
GridObjectMemberResolver(gridLoadingContext), new 
GridCache(gridLoadingContext));
-    }
-
-    @Override
-    public boolean supportsReloading() {
-        return gridCache.supportsReloading();
+        this(gridLoadingContext,
+            new 
LayoutResourceLookup(gridLoadingContext.layoutResourceLoaders()),
+            new GridLoader(gridLoadingContext),
+            new GridObjectMemberResolver(gridLoadingContext),
+            new GridCache(gridLoadingContext));
     }
 
     @Override
-    public void remove(final Class<?> domainClass) {
-        gridCache.remove(domainClass);
+    public EnumSet<CommonMimeType> supportedFormats() {
+        return context.supportedFormats();
     }
 
     @Override
-    public boolean existsFor(final Class<?> domainClass) {
-        return gridCache.existsFor(domainClass, 
gridLoadingContext.supportedFormats());
+    public Optional<GridMarshaller> marshaller(final CommonMimeType format) {
+        return context.gridMarshaller(format);
     }
 
-    @Deprecated //FIXME bad API
     @Override
-    public GridMarshaller marshaller() {
-        return 
gridLoadingContext().marshallersByMime().get(CommonMimeType.XML);
-    }
-
-    @Override
-    public BSGrid load(final Class<?> domainClass) {
-        return gridCache.load(domainClass, marshaller()).orElse(null);
+    public boolean supportsReloading() {
+        return context.supportsReloading();
     }
 
     @Override
-    public BSGrid load(final Class<?> domainClass, final String layout) {
-        return gridCache.load(domainClass, layout, marshaller()).orElse(null);
+    public void invalidate(final Class<?> domainClass) {
+        if(supportsReloading()) cache.remove(domainClass);
     }
 
-    // --
-
     @Override
-    public BSGrid defaultGridFor(final Class<?> domainClass) {
-
-        var grid = gridSystemService.defaultGrid(domainClass);
-        if(grid != null) return grid;
-
-        throw new IllegalStateException(
-                "No GridSystemService available to create grid for '" + 
domainClass.getName() + "'");
+    public BSGrid loadAndNormalize(final LayoutKey layoutKey) {
+        return cache.computeIfAbsent(layoutKey, this::loadAndNormalizeNoCache);
     }
 
     @Override
-    public BSGrid normalize(final BSGrid grid) {
-        if(grid.isNormalized()) return grid;
-
-        var domainClass = grid.domainClass();
-        gridSystemService().normalize(_Casts.uncheckedCast(grid), domainClass);
-
+    public BSGrid complete(final BSGrid grid) {
+        memberResolver().complete(grid, grid.domainClass());
         return grid;
     }
 
     @Override
-    public BSGrid complete(final BSGrid grid) {
-        var domainClass = grid.domainClass();
-        var gridSystemService = gridSystemService();
-        gridSystemService.complete(_Casts.uncheckedCast(grid), domainClass);
-
+    public BSGrid minimal(final BSGrid grid) {
+        memberResolver().minimal(grid, grid.domainClass());
         return grid;
     }
 
-    @Override
-    public BSGrid minimal(final BSGrid grid) {
-        var domainClass = grid.domainClass();
-        var gridSystemService = gridSystemService();
-        gridSystemService.minimal(_Casts.uncheckedCast(grid), domainClass);
+    // -- HELPER
 
-        return grid;
+    private BSGrid loadAndNormalizeNoCache(final LayoutKey layoutKey) {
+        return Try.call(()->layoutLookup.lookupLayoutResource(layoutKey, 
context.supportedFormats()).orElse(null))
+            
.flatMapSuccessWhenPresent(layoutResource->loader.tryLoad(layoutKey, 
layoutResource))
+            
.mapSuccess(gridOpt->gridOpt.orElseGet(()->memberResolver.defaultGrid(layoutKey.domainClass())))
+            // at this point we have a grid
+            .mapSuccessWhenPresent(grid->memberResolver.normalize(grid, 
layoutKey.domainClass()).orElse(null))
+            .valueAsNonNullElseFail();
     }
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoader.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/LayoutResourceLookup.java
similarity index 67%
copy from 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoader.java
copy to 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/LayoutResourceLookup.java
index 094fe1122ad..09f40a0f332 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoader.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/LayoutResourceLookup.java
@@ -20,38 +20,57 @@
 
 import java.util.EnumSet;
 import java.util.Optional;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
+import org.apache.causeway.applib.layout.resource.LayoutResource;
+import org.apache.causeway.applib.layout.resource.LayoutResourceLoader;
+import org.apache.causeway.applib.services.grid.GridService.LayoutKey;
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
 import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.commons.internal.reflection._Reflect;
 import 
org.apache.causeway.commons.internal.reflection._Reflect.InterfacePolicy;
-import org.apache.causeway.core.metamodel.services.grid.spi.LayoutResource;
-import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoader;
 
-record GridLoader(
-        Can<LayoutResourceLoader> layoutResourceLoaders) {
-
-    public record LayoutKey(
-        @NonNull Class<?> domainClass,
-        /** layout suffix */
-        @Nullable String layoutIfAny) {
-    }
+import lombok.extern.slf4j.Slf4j;
 
-    // -- HELPER
+/**
+ * Finds {@link LayoutResource}(s) based on domainClass and layout-suffix,
+ * by probing possible name candidates against the class-path or other sources 
(SPI).
+ *
+ * @since 4.0
+ */
+@Slf4j
+record LayoutResourceLookup(
+        Can<LayoutResourceLoader> layoutResourceLoaders) {
 
-    Optional<LayoutResource> loadLayoutResource(
+    public Optional<LayoutResource> lookupLayoutResource(
             final LayoutKey layoutKey,
             final EnumSet<CommonMimeType> supportedFormats) {
-        return _Reflect.streamTypeHierarchy(layoutKey.domainClass(), 
InterfacePolicy.EXCLUDE)
+        var layoutResourceOpt = 
_Reflect.streamTypeHierarchy(layoutKey.domainClass(), InterfacePolicy.EXCLUDE)
             .flatMap(type->loadContent(type, layoutKey.layoutIfAny(), 
supportedFormats).stream())
             .findFirst();
+
+        if(layoutResourceOpt.isPresent()) return layoutResourceOpt;
+
+        log.debug(
+            "Failed to locate or load layout resource for class {}, "
+            + "with layout-suffix (if any) {}, "
+            + "using layout-resource-loaders {}.",
+            layoutKey.domainClass().getName(), layoutKey.layoutIfAny(),
+            layoutResourceLoaders().stream()
+                .map(Object::getClass)
+                .map(Class::getName)
+                .collect(Collectors.joining(", ")));
+
+        return Optional.empty();
     }
 
+    // -- HELPER
+
     private Optional<LayoutResource> loadContent(
             final @NonNull Class<?> domainClass,
             final @Nullable String layoutIfAny,
@@ -85,13 +104,13 @@ private Stream<String> streamResourceNameCandidatesFor(
         var typeSimpleName = domainClass.getSimpleName();
 
         return _Strings.isNotEmpty(layoutIfAny)
-                ? Stream.of(
-                        String.format("%s-%s.layout.%s", typeSimpleName, 
layoutIfAny, fileExtension),
-                        String.format("%s.layout.%s", typeSimpleName, 
fileExtension),
-                        String.format("%s.layout.fallback.%s", typeSimpleName, 
fileExtension))
-                : Stream.of(
-                        String.format("%s.layout.%s", typeSimpleName, 
fileExtension),
-                        String.format("%s.layout.fallback.%s", 
typeSimpleName,fileExtension));
+            ? Stream.of(
+                    String.format("%s-%s.layout.%s", typeSimpleName, 
layoutIfAny, fileExtension),
+                    String.format("%s.layout.%s", typeSimpleName, 
fileExtension),
+                    String.format("%s.layout.fallback.%s", typeSimpleName, 
fileExtension))
+            : Stream.of(
+                    String.format("%s.layout.%s", typeSimpleName, 
fileExtension),
+                    String.format("%s.layout.fallback.%s", 
typeSimpleName,fileExtension));
     }
 
     private Optional<LayoutResource> lookupLayoutResourceUsingLoaders(
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoaderDefault.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoaderDefault.java
index ea5e29c4689..2dbaa89ae1d 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoaderDefault.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoaderDefault.java
@@ -27,6 +27,8 @@
 import org.springframework.stereotype.Service;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.applib.layout.resource.LayoutResource;
+import org.apache.causeway.applib.layout.resource.LayoutResourceLoader;
 import org.apache.causeway.applib.value.NamedWithMimeType;
 import org.apache.causeway.commons.functional.Try;
 import org.apache.causeway.commons.io.DataSource;
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/layout/LayoutServiceDefault.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/layout/LayoutServiceDefault.java
index abafd08d4fa..5e8b0b01732 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/layout/LayoutServiceDefault.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/layout/LayoutServiceDefault.java
@@ -83,7 +83,7 @@ public String menuBarsLayout(
 
     @Override
     public EnumSet<CommonMimeType> supportedObjectLayoutFormats() {
-        return gridService.marshaller().supportedFormats();
+        return gridService.supportedFormats();
     }
 
     @Override
@@ -131,8 +131,9 @@ private Try<String> tryGridToFormatted(
 
     private String gridToFormatted(final @Nullable BSGrid grid, final 
CommonMimeType format) {
         if(grid==null) return null;
-
-        return gridService.marshaller().marshal(_Casts.uncheckedCast(grid), 
format);
+        return gridService.marshaller(format)
+            .map(marshaller->marshaller.marshal(grid, format))
+            .orElse(null);
     }
 
     private static String zipEntryNameFor(
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java
index 12da736ef91..b2232bbd58f 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java
@@ -115,8 +115,7 @@ public Optional<LogicalType> lookupLogicalTypeByClass(final 
@Nullable Class<?> d
 
     @Override
     public void rebuild(final Class<?> domainType) {
-
-        gridService.remove(domainType);
+        gridService.invalidate(domainType);
         specificationLoader().reloadSpecification(domainType);
     }
 
diff --git 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridCache_resourceNameTest.java
 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridCache_resourceNameTest.java
index 02f3a885a2c..9424191bf45 100644
--- 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridCache_resourceNameTest.java
+++ 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridCache_resourceNameTest.java
@@ -19,82 +19,52 @@
 package org.apache.causeway.core.metamodel.services.grid;
 
 import java.util.EnumSet;
-import java.util.Map;
 
-import jakarta.inject.Provider;
-
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
-import org.apache.causeway.applib.services.grid.GridMarshaller;
-import org.apache.causeway.applib.services.message.MessageService;
+import org.apache.causeway.applib.layout.resource.LayoutResource;
+import org.apache.causeway.applib.services.grid.GridService.LayoutKey;
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
 import org.apache.causeway.commons.collections.Can;
-import org.apache.causeway.core.config.CausewayConfiguration;
-import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
-import org.apache.causeway.core.metamodel.services.grid.GridLoader.LayoutKey;
-import 
org.apache.causeway.core.metamodel.services.grid.GridObjectMemberResolver.FallbackLayoutDataSource;
-import org.apache.causeway.core.metamodel.services.grid.spi.LayoutResource;
-import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoader;
 import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoaderDefault;
-import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
 
 class GridCache_resourceNameTest {
 
-    private GridCache gridCache;
-    private LayoutResourceLoader layoutResourceLoader;
-
-    @BeforeEach
-    void setUp() throws Exception {
-        layoutResourceLoader = new LayoutResourceLoaderDefault();
-
-        var ctx = new GridLoadingContext(
-            (CausewaySystemEnvironment) null,
-            (CausewayConfiguration) null,
-            (MessageService) null,
-            (Provider<SpecificationLoader>) null,
-            (Map<CommonMimeType, GridMarshaller>) null,
-            Can.of(layoutResourceLoader),
-            Can.<FallbackLayoutDataSource>empty(),
-            false); // reloading supported
-
-        gridCache = new GridCache(ctx);
-    }
-
     @Test
     void when_default_exists() {
         assertEquals(
                 "Foo.layout.xml",
-                resourceNameFor(new GridLoader.LayoutKey(Foo.class, null)));
+                resourceNameFor(new LayoutKey(Foo.class, null)));
     }
 
     @Test
     void when_fallback_exists() {
         assertEquals(
                 "Foo2.layout.fallback.xml",
-                resourceNameFor(new GridLoader.LayoutKey(Foo2.class, null)));
+                resourceNameFor(new LayoutKey(Foo2.class, null)));
     }
 
     @Test
     void when_default_and_fallback_both_exist() {
         assertEquals(
                 "Foo3.layout.xml",
-                resourceNameFor(new GridLoader.LayoutKey(Foo3.class, null)));
+                resourceNameFor(new LayoutKey(Foo3.class, null)));
     }
 
     @Test
     void when_neither_exist() {
         assertEquals(
                 (String)null,
-                resourceNameFor(new GridLoader.LayoutKey(Foo4.class, null)));
+                resourceNameFor(new LayoutKey(Foo4.class, null)));
     }
 
     // -- HELPER
 
-    private String resourceNameFor(final LayoutKey dcal) {
-        return gridCache.gridLoader().loadLayoutResource(dcal, 
EnumSet.of(CommonMimeType.XML))
+    private String resourceNameFor(final LayoutKey key) {
+        var resourceLookup = new LayoutResourceLookup(Can.of(new 
LayoutResourceLoaderDefault()));
+        return resourceLookup.lookupLayoutResource(key, 
EnumSet.of(CommonMimeType.XML))
             .map(LayoutResource::resourceName)
             .orElse(null);
     }
diff --git 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingTest.java
 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingTest.java
index aeda1067fa7..be4cb6adca1 100644
--- 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingTest.java
+++ 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingTest.java
@@ -18,8 +18,6 @@
  */
 package org.apache.causeway.core.metamodel.services.grid;
 
-import java.util.EnumSet;
-
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -29,9 +27,6 @@
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.apache.causeway.applib.services.grid.GridService;
-import org.apache.causeway.applib.services.layout.LayoutExportStyle;
-import org.apache.causeway.applib.services.layout.LayoutService;
-import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
 import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
 import org.apache.causeway.core.metamodel.MetaModelTestAbstract;
 import org.apache.causeway.core.metamodel.facetapi.Facet.Precedence;
@@ -43,8 +38,7 @@
 class GridLoadingTest
 extends MetaModelTestAbstract {
 
-    private GridCache gridCache;
-    private LayoutService layoutService;
+    private GridServiceDefault gridService;
 
     @Override
     protected void onSetUp(final MetaModelContext_forTestingBuilder 
mmcBuilder) {
@@ -58,20 +52,9 @@ protected void onSetUp(final 
MetaModelContext_forTestingBuilder mmcBuilder) {
 
     @Override
     protected void afterSetUp() {
-        layoutService = 
getServiceRegistry().lookupServiceElseFail(LayoutService.class);
-        gridCache = ((GridServiceDefault) getServiceRegistry()
-                .lookupServiceElseFail(GridService.class))
-                .gridCache();
-        assertTrue(gridCache.supportsReloading());
-    }
-
-    // test blueprint, for future work
-    void blueprint() {
-        var domainClassAndLayout = new GridLoader.LayoutKey(Bar.class, null);
-        gridCache.gridLoader().loadLayoutResource(domainClassAndLayout, 
EnumSet.of(CommonMimeType.XML));
-
-        var xml = layoutService.objectLayout(Bar.class, 
LayoutExportStyle.MINIMAL, CommonMimeType.XML);
-        System.out.println(xml);
+        this.gridService = ((GridServiceDefault) getServiceRegistry()
+                .lookupServiceElseFail(GridService.class));
+        assertTrue(gridService.supportsReloading());
     }
 
     @Test
@@ -97,6 +80,8 @@ void customNamed() {
         // verify however, that the number of facets stays constant
 
         // triggers grid to be re-loaded
+        gridService.invalidate(Bar.class);
+
         var grid2 = gridFacet.getGrid(ManagedObject.adaptSingular(barSpec, new 
Bar()));
         assertNotSame(grid, grid2); // verify that we actually got a new grid, 
indicative of a reload having taken place
 
diff --git 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridXmlRoundtripTest.java
 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridXmlRoundtripTest.java
index c08a713790d..6663e554d8d 100644
--- 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridXmlRoundtripTest.java
+++ 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridXmlRoundtripTest.java
@@ -36,7 +36,6 @@
 import org.apache.causeway.applib.services.jaxb.CausewaySchemas;
 import org.apache.causeway.applib.services.jaxb.JaxbService;
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
-import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.testing._DocumentTester;
 import org.apache.causeway.core.metamodel.MetaModelTestAbstract;
 
@@ -119,14 +118,16 @@ void happy_case() throws Exception {
         tabRightCol.getCollections().add(similarToColl);
         similarToColl.setId("similarTo");
 
-        String xml = 
gridServiceDefault.marshaller().marshal(_Casts.uncheckedCast(bsGrid), 
CommonMimeType.XML);
+        var xmlMarshaller = 
gridServiceDefault.marshaller(CommonMimeType.XML).orElseThrow();
+
+        String xml = xmlMarshaller.marshal(bsGrid, CommonMimeType.XML);
 
         println(xml);
 
-        BSGrid bsGridRoundtripped = (BSGrid) 
gridServiceDefault.marshaller().unmarshal(Object.class, xml, CommonMimeType.XML)
+        BSGrid bsGridRoundtripped = xmlMarshaller.unmarshal(Object.class, xml, 
CommonMimeType.XML)
             .valueAsNonNullElseFail();
 
-        String xmlRoundtripped = 
gridServiceDefault.marshaller().marshal(_Casts.uncheckedCast(bsGridRoundtripped),
 CommonMimeType.XML);
+        String xmlRoundtripped = xmlMarshaller.marshal(bsGridRoundtripped, 
CommonMimeType.XML);
 
         _DocumentTester.assertXmlEqualsIgnoreOrder(xml, xmlRoundtripped);
 
diff --git 
a/extensions/core/layoutloaders/github/src/main/java/org/apache/causeway/extensions/layoutloaders/github/spiimpl/LayoutResourceLoaderFromGithub.java
 
b/extensions/core/layoutloaders/github/src/main/java/org/apache/causeway/extensions/layoutloaders/github/spiimpl/LayoutResourceLoaderFromGithub.java
index 82a8f075c46..10399968ff3 100644
--- 
a/extensions/core/layoutloaders/github/src/main/java/org/apache/causeway/extensions/layoutloaders/github/spiimpl/LayoutResourceLoaderFromGithub.java
+++ 
b/extensions/core/layoutloaders/github/src/main/java/org/apache/causeway/extensions/layoutloaders/github/spiimpl/LayoutResourceLoaderFromGithub.java
@@ -34,12 +34,12 @@
 import org.springframework.web.client.RestTemplate;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.applib.layout.resource.LayoutResource;
+import org.apache.causeway.applib.layout.resource.LayoutResourceLoader;
 import org.apache.causeway.applib.services.queryresultscache.QueryResultsCache;
 import org.apache.causeway.applib.value.NamedWithMimeType;
 import org.apache.causeway.commons.functional.Try;
 import org.apache.causeway.core.config.CausewayConfiguration;
-import org.apache.causeway.core.metamodel.services.grid.spi.LayoutResource;
-import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoader;
 import 
org.apache.causeway.extensions.layoutloaders.github.CausewayModuleExtLayoutLoadersGithub;
 import 
org.apache.causeway.extensions.layoutloaders.github.menu.LayoutLoadersGitHubMenu;
 
diff --git 
a/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/ResourceController.java
 
b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/ResourceController.java
index b74d74b5cf7..5b098808e87 100644
--- 
a/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/ResourceController.java
+++ 
b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/ResourceController.java
@@ -41,7 +41,6 @@
 import org.apache.causeway.applib.value.Blob;
 import org.apache.causeway.applib.value.Clob;
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
-import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.core.metamodel.facets.object.grid.GridFacet;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
@@ -190,7 +189,7 @@ public ResponseEntity<byte[]> icon(
     private Optional<String> gridAsXml(final ManagedObject managedObject) {
         return managedObject.objSpec().lookupFacet(GridFacet.class)
             .map(facet->facet.getGrid(managedObject))
-            
.map(grid->gridService().marshaller().marshal(_Casts.uncheckedCast(grid), 
CommonMimeType.XML))
+            
.flatMap(grid->gridService().marshaller(CommonMimeType.XML).map(mars->mars.marshal(grid,
 CommonMimeType.XML)))
             .map(x -> x.replaceAll("(\r\n)", "\n"));
     }
 

Reply via email to