You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2020/01/05 10:17:45 UTC

[isis] 01/02: ISIS-2250: adds config properties to lock down metamodel, or to incrementally validate otherwise.

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

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

commit 61e4f55704028ca1ae777e80af48d1cdf49b7142
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Jan 5 10:09:37 2020 +0000

    ISIS-2250: adds config properties to lock down metamodel, or to incrementally validate otherwise.
    
    Also:
    - adds validator to ensure that there are no actions except on known types
    - no longer inject into Interaction, instead pass in the required services (ClockService, MetricsService).
---
 .../isis/applib/services/command/Command.java      |  2 +-
 .../isis/applib/services/iactn/Interaction.java    | 56 +++++++++++++---------
 .../commons/internal/exceptions/_Exceptions.java   | 12 ++++-
 .../org/apache/isis/config/IsisConfiguration.java  | 48 ++++++++++++++++++-
 .../isis/config/beans/IsisBeanTypeRegistry.java    | 11 +++--
 .../isis/metamodel/facets/DomainEventHelper.java   |  2 +-
 ...ctionInvocationFacetForDomainEventAbstract.java |  9 ++--
 ...tySetterOrClearFacetForDomainEventAbstract.java | 13 +++--
 .../isis/metamodel/progmodel/ProgrammingModel.java | 10 ++++
 .../dflt/ProgrammingModelFacetsJava8.java          | 22 +++++++++
 .../isis/metamodel/spec/ObjectSpecification.java   |  1 +
 .../metamodel/specloader/SpecificationLoader.java  |  5 +-
 .../specloader/SpecificationLoaderDefault.java     | 56 +++++++++++++++++-----
 .../specimpl/ObjectSpecificationAbstract.java      | 13 ++++-
 .../specimpl/dflt/ObjectSpecificationDefault.java  |  2 +
 .../validator/MetaModelValidatorVisiting.java      | 46 +++++++++++++++---
 .../metamodel/JdoProgrammingModelPlugin.java       |  2 +
 .../persistence/IsisTransactionJdo.java            |  7 +--
 .../persistence/PersistenceSession5.java           |  2 +-
 19 files changed, 250 insertions(+), 69 deletions(-)

diff --git a/core/applib/src/main/java/org/apache/isis/applib/services/command/Command.java b/core/applib/src/main/java/org/apache/isis/applib/services/command/Command.java
index fe83936..1eb9ff2 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/services/command/Command.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/services/command/Command.java
@@ -390,7 +390,7 @@ public interface Command extends HasUniqueId {
          * </p>
          *
          * See also {@link Interaction#getCurrentExecution()} and
-         * {@link Interaction.Execution#setStartedAt(Timestamp)}.
+         * {@link #setStartedAt(org.apache.isis.applib.services.clock.ClockService, org.apache.isis.applib.services.metrics.MetricsService)}.
          */
         void setStartedAt(Timestamp startedAt);
 
diff --git a/core/applib/src/main/java/org/apache/isis/applib/services/iactn/Interaction.java b/core/applib/src/main/java/org/apache/isis/applib/services/iactn/Interaction.java
index 4cb19a9..d53e822 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/services/iactn/Interaction.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/services/iactn/Interaction.java
@@ -27,10 +27,7 @@ import java.util.UUID;
 import java.util.concurrent.Callable;
 import java.util.concurrent.atomic.LongAdder;
 
-import javax.inject.Inject;
-
 import org.apache.isis.applib.annotation.Programmatic;
-import org.apache.isis.applib.annotation.Value;
 import org.apache.isis.applib.events.domain.AbstractDomainEvent;
 import org.apache.isis.applib.events.domain.ActionDomainEvent;
 import org.apache.isis.applib.events.domain.PropertyDomainEvent;
@@ -40,7 +37,6 @@ import org.apache.isis.applib.services.command.Command;
 import org.apache.isis.applib.services.eventbus.EventBusService;
 import org.apache.isis.applib.services.metrics.MetricsService;
 import org.apache.isis.applib.services.wrapper.WrapperFactory;
-import org.apache.isis.applib.services.xactn.Transaction;
 import org.apache.isis.applib.util.schema.MemberExecutionDtoUtils;
 import org.apache.isis.commons.internal.collections._Lists;
 import org.apache.isis.commons.internal.collections._Maps;
@@ -54,6 +50,8 @@ import org.apache.isis.schema.ixn.v1.ObjectCountsDto;
 import org.apache.isis.schema.ixn.v1.PropertyEditDto;
 import org.apache.isis.schema.jaxbadapters.JavaSqlTimestampXmlGregorianCalendarAdapter;
 
+import lombok.val;
+
 /**
  * Represents an action invocation or property modification, resulting in some state change of the system.  It captures
  * not only the target object and arguments passed, but also builds up the call-graph, and captures metrics, eg
@@ -79,7 +77,6 @@ import org.apache.isis.schema.jaxbadapters.JavaSqlTimestampXmlGregorianCalendarA
  * </p>
  *
  */
-@Value
 public class Interaction implements HasUniqueId {
 
     // -- transactionId (property)
@@ -135,11 +132,13 @@ public class Interaction implements HasUniqueId {
     @Programmatic
     public Object execute(
             final MemberExecutor<ActionInvocation> memberExecutor,
-            final ActionInvocation actionInvocation) {
+            final ActionInvocation actionInvocation,
+            final ClockService clockService,
+            final MetricsService metricsService) {
 
         push(actionInvocation);
 
-        return executeInternal(memberExecutor, actionInvocation);
+        return executeInternal(memberExecutor, actionInvocation, clockService, metricsService);
     }
 
     /**
@@ -153,16 +152,20 @@ public class Interaction implements HasUniqueId {
     @Programmatic
     public Object execute(
             final MemberExecutor<PropertyEdit> memberExecutor,
-            final PropertyEdit propertyEdit) {
+            final PropertyEdit propertyEdit,
+            final ClockService clockService,
+            final MetricsService metricsService) {
 
         push(propertyEdit);
 
-        return executeInternal(memberExecutor, propertyEdit);
+        return executeInternal(memberExecutor, propertyEdit, clockService, metricsService);
     }
 
     private <T extends Execution<?,?>> Object executeInternal(
             final MemberExecutor<T> memberExecutor,
-            final T execution) {
+            final T execution,
+            final ClockService clockService,
+            final MetricsService metricsService) {
 
         // as a convenience, since in all cases we want the command to start when the first 
         // interaction executes, we populate the command here.
@@ -190,7 +193,7 @@ public class Interaction implements HasUniqueId {
             }
         } finally {
             final Timestamp completedAt = clockService.nowAsJavaSqlTimestamp();
-            pop(completedAt);
+            pop(completedAt, metricsService);
         }
     }
 
@@ -238,12 +241,14 @@ public class Interaction implements HasUniqueId {
      * </p>
      */
     @Programmatic
-    private Execution<?,?> pop(final Timestamp completedAt) {
+    private Execution<?,?> pop(
+            final Timestamp completedAt,
+            final MetricsService metricsService) {
         if(currentExecution == null) {
             throw new IllegalStateException("No current execution to pop");
         }
         final Execution<?,?> popped = currentExecution;
-        popped.setCompletedAt(completedAt);
+        popped.setCompletedAt(completedAt, metricsService);
 
         moveCurrentTo(currentExecution.getParent());
         return popped;
@@ -301,7 +306,7 @@ public class Interaction implements HasUniqueId {
          */
         PUBLISHED_EVENT,
         /**
-         * There may be multiple transactions within a given interaction, as per {@link Transaction#getSequence()}.
+         * There may be multiple transactions within a given interaction.
          */
         TRANSACTION,
         ;
@@ -483,8 +488,12 @@ public class Interaction implements HasUniqueId {
         }
 
         @Programmatic
-        public void setStartedAt(final Timestamp startedAt) {
-            syncMetrics(When.BEFORE, startedAt);
+        public Timestamp start(
+                final ClockService clockService,
+                final MetricsService metricsService) {
+            val startedAt = clockService.nowAsJavaSqlTimestamp();
+            syncMetrics(When.BEFORE, startedAt, metricsService);
+            return startedAt;
         }
 
 
@@ -499,8 +508,10 @@ public class Interaction implements HasUniqueId {
         /**
          * <b>NOT API</b>: intended to be called only by the framework.
          */
-        void setCompletedAt(final Timestamp completedAt) {
-            syncMetrics(When.AFTER, completedAt);
+        void setCompletedAt(
+                final Timestamp completedAt,
+                final MetricsService metricsService) {
+            syncMetrics(When.AFTER, completedAt, metricsService);
         }
 
 
@@ -651,8 +662,10 @@ public class Interaction implements HasUniqueId {
                     final int numberObjectsLoaded,
                     final int numberObjectsDirtied);
         }
-        private void syncMetrics(final When when, final Timestamp timestamp) {
-            final MetricsService metricsService = interaction.metricsService;
+        private void syncMetrics(
+                final When when,
+                final Timestamp timestamp,
+                final MetricsService metricsService) {
 
             final int numberObjectsLoaded = metricsService.numberObjectsLoaded();
             final int numberObjectsDirtied = metricsService.numberObjectsDirtied();
@@ -706,7 +719,4 @@ public class Interaction implements HasUniqueId {
         }
     }
 
-    @Inject MetricsService metricsService;
-    @Inject ClockService clockService;
-
 }
diff --git a/core/commons/src/main/java/org/apache/isis/commons/internal/exceptions/_Exceptions.java b/core/commons/src/main/java/org/apache/isis/commons/internal/exceptions/_Exceptions.java
index 8598579..974396d 100644
--- a/core/commons/src/main/java/org/apache/isis/commons/internal/exceptions/_Exceptions.java
+++ b/core/commons/src/main/java/org/apache/isis/commons/internal/exceptions/_Exceptions.java
@@ -74,11 +74,21 @@ public final class _Exceptions {
      * @param _case the unmatched case to be reported
      * @return
      */
-    public static final IllegalArgumentException illegalArgument(String format, @Nullable Object ... args) {
+    public static final IllegalArgumentException illegalArgument(
+            final String format,
+            final @Nullable Object ... args) {
         requires(format, "format");
         return new IllegalArgumentException(String.format(format, args));
     }
 
+    public static IllegalStateException illegalState(
+            final String format,
+            final @Nullable Object ... args) {
+        requires(format, "format");
+        return new IllegalStateException(String.format(format, args));
+    }
+
+
     public static final NoSuchElementException noSuchElement(String msg) {
         return new NoSuchElementException(msg);
     }
diff --git a/core/config/src/main/java/org/apache/isis/config/IsisConfiguration.java b/core/config/src/main/java/org/apache/isis/config/IsisConfiguration.java
index 9a66511..78db370 100644
--- a/core/config/src/main/java/org/apache/isis/config/IsisConfiguration.java
+++ b/core/config/src/main/java/org/apache/isis/config/IsisConfiguration.java
@@ -501,8 +501,52 @@ public class IsisConfiguration {
         private final Introspector introspector = new Introspector();
         @Data
         public static class Introspector {
+            /**
+             * Whether to perform introspection and metamodel validation in parallel.
+             */
             private boolean parallelize = true;
+            /**
+             * Whether all known types should be fully introspected as part of the bootstrapping, or should only be
+             * partially introspected initially.
+             *
+             * <p>
+             * Leaving this as lazy means that there's a chance that metamodel validation errors will not be
+             * discovered during bootstrap.  That said, metamodel validation is still run incrementally for any
+             * classes introspected lazily after initial bootstrapping (unless {@link #isValidateIncrementally()} is
+             * disabled.
+             * </p>
+             */
             private IntrospectionMode mode = IntrospectionMode.LAZY_UNLESS_PRODUCTION;
+            /**
+             * If true, then no new specifications will be allowed to be loaded once introspection has been complete.
+             *
+             * <p>
+             * Only applies if the introspector is configured to perform full introspection up-front (either because of
+             * {@link IntrospectionMode#FULL} or {@link IntrospectionMode#LAZY_UNLESS_PRODUCTION} when in production);
+             * otherwise is ignored.
+             * </p>
+             */
+            private boolean lockAfterFullIntrospection = true;
+            /**
+             * If true, then metamodel validation is performed after any new specification has been loaded (after the
+             * initial bootstrapping).
+             *
+             * <p>
+             * This does <i>not</i> apply if the introspector is configured to perform full introspection up-front
+             * AND when the metamodel is {@link #isLockAfterFullIntrospection() locked} after initial bootstrapping
+             * (because in that case the lock check will simply prevent any new specs from being loaded).
+             * But it will apply otherwise.
+             * </p>
+             *
+             * <p>In particular, this setting <i>can</i> still apply even if the {@link #getMode() introspection mode}
+             * is set to {@link IntrospectionMode#FULL full}, because that in itself does not preclude some code
+             * from attempting to load some previously unknown type.  For example, a fixture script could attempt to
+             * invoke an action on some new type using the
+             * {@link org.apache.isis.applib.services.wrapper.WrapperFactory} - this will cause introspection of that
+             * new type to be performed.
+             * </p>
+             */
+            private boolean validateIncrementally = true;
         }
 
         private final Validator validator = new Validator();
@@ -1122,7 +1166,7 @@ public class IsisConfiguration {
              * eg: {@code isis.value.format.datetime=iso}
              * <p>
              * A pre-determined list of values is available, specifically 'iso_encoding', 'iso' and 'medium' (see
-             * {@link org.apache.isis.metamodel.facets.value.datetimejdk8local.Jdk8LocalDateTimeValueSemanticsProvider.NAMED_TITLE_FORMATTERS}).  
+             * <code>org.apache.isis.metamodel.facets.value.datetimejdk8local.Jdk8LocalDateTimeValueSemanticsProvider#NAMED_TITLE_FORMATTERS</code>).
              * Alternatively, can also specify a mask, eg <tt>dd-MMM-yyyy</tt>.
              */
             DATETIME,
@@ -1132,7 +1176,7 @@ public class IsisConfiguration {
              * eg: {@code isis.value.format.date=iso}
              * <p>
              * A pre-determined list of values is available, specifically 'iso_encoding', 'iso' and 'medium' (see
-             * {@link org.apache.isis.metamodel.facets.value.datejdk8local.Jdk8LocalDateValueSemanticsProvider.NAMED_TITLE_FORMATTERS}).  
+             * <code>org.apache.isis.metamodel.facets.value.datejdk8local.Jdk8LocalDateValueSemanticsProvider.NAMED_TITLE_FORMATTERS</code>).
              * Alternatively,  can also specify a mask, eg <tt>dd-MMM-yyyy</tt>.
              */
             DATE, 
diff --git a/core/config/src/main/java/org/apache/isis/config/beans/IsisBeanTypeRegistry.java b/core/config/src/main/java/org/apache/isis/config/beans/IsisBeanTypeRegistry.java
index 1107a2b..073060b 100644
--- a/core/config/src/main/java/org/apache/isis/config/beans/IsisBeanTypeRegistry.java
+++ b/core/config/src/main/java/org/apache/isis/config/beans/IsisBeanTypeRegistry.java
@@ -162,15 +162,16 @@ public final class IsisBeanTypeRegistry implements IsisComponentScanInterceptor,
     /**
      * If given type is part of the meta-model and is available for injection, 
      * returns the <em>Managed Bean's</em> name (id) as
-     * recognized by the IoC container, {@code null} otherwise;
+     * recognized by the IoC container.
+     *
      * @param type
      * @return
      */
-    public String getManagedBeanNameForType(Class<?> type) {
+    public Optional<String> getManagedBeanNameForType(Class<?> type) {
         if(vetoedTypes.contains(type)) { // vetos are coming from the spec-loader during init
-            return null;
+            return Optional.empty();
         }
-        return managedBeanNamesByType.get(type);
+        return Optional.ofNullable(managedBeanNamesByType.get(type));
     }
     
     /**
@@ -179,7 +180,7 @@ public final class IsisBeanTypeRegistry implements IsisComponentScanInterceptor,
      * @param type
      */
     public boolean isManagedBean(Class<?> type) {
-        return getManagedBeanNameForType(type)!=null;
+        return getManagedBeanNameForType(type).isPresent();
     }
     
     // -- HELPER
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/DomainEventHelper.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/DomainEventHelper.java
index 1b736bd..067ecb2 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/DomainEventHelper.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/DomainEventHelper.java
@@ -132,7 +132,7 @@ public class DomainEventHelper {
                             .map(ObjectActionParameter::getName)
                             .collect(_Lists.toUnmodifiable());
 
-                    val parameterTypes = parameters.stream()
+                    final List<Class<?>> parameterTypes = parameters.stream()
                             .map(ObjectActionParameter::getSpecification)
                             .map(ObjectSpecification::getCorrespondingClass)
                             .collect(_Lists.toUnmodifiable());
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/actions/action/invocation/ActionInvocationFacetForDomainEventAbstract.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/actions/action/invocation/ActionInvocationFacetForDomainEventAbstract.java
index 6893538..d9e2900 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/actions/action/invocation/ActionInvocationFacetForDomainEventAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/actions/action/invocation/ActionInvocationFacetForDomainEventAbstract.java
@@ -44,6 +44,7 @@ import org.apache.isis.applib.services.iactn.Interaction.ActionInvocation;
 import org.apache.isis.applib.services.iactn.InteractionContext;
 import org.apache.isis.applib.services.metamodel.MetaModelService;
 import org.apache.isis.applib.services.metamodel.MetaModelService.Mode;
+import org.apache.isis.applib.services.metrics.MetricsService;
 import org.apache.isis.applib.services.queryresultscache.QueryResultsCache;
 import org.apache.isis.applib.services.registry.ServiceRegistry;
 import org.apache.isis.commons.collections.Can;
@@ -205,7 +206,7 @@ implements ImperativeFacet {
                             mixinElseRegularAdapter, mixedInAdapter, execution);
 
             // sets up startedAt and completedAt on the execution, also manages the execution call graph
-            interaction.execute(callable, execution);
+            interaction.execute(callable, execution, getClockService(), getMetricsService());
 
             // handle any exceptions
             final Interaction.Execution<ActionInvocationDto, ?> priorExecution =
@@ -407,6 +408,9 @@ implements ImperativeFacet {
     private ClockService getClockService() {
         return serviceRegistry.lookupServiceElseFail(ClockService.class);
     }
+    private MetricsService getMetricsService() {
+        return serviceRegistry.lookupServiceElseFail(MetricsService.class);
+    }
 
     private PublisherDispatchService getPublishingServiceInternal() {
         return serviceRegistry.lookupServiceElseFail(PublisherDispatchService.class);
@@ -444,8 +448,7 @@ implements ImperativeFacet {
                 // set the startedAt (and update command if this is the top-most member execution)
                 // (this isn't done within Interaction#execute(...) because it requires the DTO
                 // to have been set on the current execution).
-                val startedAt = getClockService().nowAsJavaSqlTimestamp();
-                execution.setStartedAt(startedAt);
+                val startedAt = execution.start(getClockService(), getMetricsService());
                 if(command.getStartedAt() == null) {
                     command.internal().setStartedAt(startedAt);
                 }
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/properties/property/modify/PropertySetterOrClearFacetForDomainEventAbstract.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/properties/property/modify/PropertySetterOrClearFacetForDomainEventAbstract.java
index f3ce1e8..b8c1a7f 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/properties/property/modify/PropertySetterOrClearFacetForDomainEventAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/properties/property/modify/PropertySetterOrClearFacetForDomainEventAbstract.java
@@ -19,7 +19,6 @@
 
 package org.apache.isis.metamodel.facets.properties.property.modify;
 
-import java.sql.Timestamp;
 import java.util.Map;
 import java.util.Objects;
 
@@ -31,6 +30,7 @@ import org.apache.isis.applib.services.command.CommandContext;
 import org.apache.isis.applib.services.command.spi.CommandService;
 import org.apache.isis.applib.services.iactn.Interaction;
 import org.apache.isis.applib.services.iactn.InteractionContext;
+import org.apache.isis.applib.services.metrics.MetricsService;
 import org.apache.isis.commons.exceptions.IsisException;
 import org.apache.isis.metamodel.consent.InteractionInitiatedBy;
 import org.apache.isis.metamodel.facetapi.Facet;
@@ -50,6 +50,8 @@ import org.apache.isis.schema.ixn.v1.PropertyEditDto;
 
 import static org.apache.isis.commons.internal.base._Casts.uncheckedCast;
 
+import lombok.val;
+
 public abstract class PropertySetterOrClearFacetForDomainEventAbstract
 extends SingleValueFacetAbstract<Class<? extends PropertyDomainEvent<?,?>>> {
 
@@ -222,8 +224,7 @@ extends SingleValueFacetAbstract<Class<? extends PropertyDomainEvent<?,?>>> {
                         // set the startedAt (and update command if this is the top-most member execution)
                         // (this isn't done within Interaction#execute(...) because it requires the DTO
                         // to have been set on the current execution).
-                        final Timestamp startedAt = getClockService().nowAsJavaSqlTimestamp();
-                        execution.setStartedAt(startedAt);
+                        val startedAt = execution.start(getClockService(), getMetricsService());
                         if(command.getStartedAt() == null) {
                             command.internal().setStartedAt(startedAt);
                         }
@@ -274,7 +275,7 @@ extends SingleValueFacetAbstract<Class<? extends PropertyDomainEvent<?,?>>> {
             };
 
             // sets up startedAt and completedAt on the execution, also manages the execution call graph
-            interaction.execute(executor, execution);
+            interaction.execute(executor, execution, getClockService(), getMetricsService());
 
             // handle any exceptions
             final Interaction.Execution<?, ?> priorExecution = interaction.getPriorExecution();
@@ -322,6 +323,10 @@ extends SingleValueFacetAbstract<Class<? extends PropertyDomainEvent<?,?>>> {
         return getServiceRegistry().lookupServiceElseFail(ClockService.class);
     }
 
+    private MetricsService getMetricsService() {
+        return getServiceRegistry().lookupServiceElseFail(MetricsService.class);
+    }
+
     private PublisherDispatchService getPublishingServiceInternal() {
         return getServiceRegistry().lookupServiceElseFail(PublisherDispatchService.class);
     }
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodel/ProgrammingModel.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodel/ProgrammingModel.java
index 9e237b3..8ba832d 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodel/ProgrammingModel.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodel/ProgrammingModel.java
@@ -19,14 +19,18 @@
 
 package org.apache.isis.metamodel.progmodel;
 
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.stream.Stream;
 
 import org.apache.isis.commons.internal.functions._Functions;
 import org.apache.isis.metamodel.facets.FacetFactory;
+import org.apache.isis.metamodel.spec.ObjectSpecification;
 import org.apache.isis.metamodel.specloader.validator.MetaModelValidator;
 import org.apache.isis.metamodel.specloader.validator.MetaModelValidatorVisiting;
 
+import lombok.NonNull;
+
 public interface ProgrammingModel {
 
     // -- ENUM TYPES
@@ -155,6 +159,12 @@ public interface ProgrammingModel {
         addValidator(MetaModelValidatorVisiting.of(visitor), markers);
     }
     
+    default void addValidator(final MetaModelValidatorVisiting.Visitor visitor,
+                              final @NonNull Predicate<ObjectSpecification> specPredicate,
+                              final Marker ... markers) {
+        addValidator(MetaModelValidatorVisiting.of(visitor, specPredicate), markers);
+    }
+
     /** shortcut for see {@link #addPostProcessor(PostProcessingOrder, Class, Supplier, Marker...)}*/
     default <T extends ObjectSpecificationPostProcessor> void addPostProcessor(
             PostProcessingOrder order, 
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodels/dflt/ProgrammingModelFacetsJava8.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodels/dflt/ProgrammingModelFacetsJava8.java
index bd5eb4b..77fb34d 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodels/dflt/ProgrammingModelFacetsJava8.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodels/dflt/ProgrammingModelFacetsJava8.java
@@ -17,8 +17,13 @@
 
 package org.apache.isis.metamodel.progmodels.dflt;
 
+import java.util.function.Predicate;
+
+import org.apache.isis.applib.Identifier;
 import org.apache.isis.applib.services.inject.ServiceInjector;
+import org.apache.isis.commons.internal.ioc.BeanSort;
 import org.apache.isis.metamodel.authorization.standard.AuthorizationFacetFactory;
+import org.apache.isis.metamodel.facetapi.FacetHolder;
 import org.apache.isis.metamodel.facets.OrphanedSupportingMethodValidator;
 import org.apache.isis.metamodel.facets.actions.action.ActionAnnotationFacetFactory;
 import org.apache.isis.metamodel.facets.actions.action.ActionChoicesForCollectionParameterFacetFactory;
@@ -145,6 +150,12 @@ import org.apache.isis.metamodel.facets.value.uuid.UUIDValueFacetUsingSemanticsP
 import org.apache.isis.metamodel.postprocessors.param.DeriveFacetsPostProcessor;
 import org.apache.isis.metamodel.progmodel.ProgrammingModelAbstract;
 import org.apache.isis.metamodel.services.title.TitlesAndTranslationsValidator;
+import org.apache.isis.metamodel.spec.ObjectSpecification;
+import org.apache.isis.metamodel.spec.feature.Contributed;
+import org.apache.isis.metamodel.specloader.validator.MetaModelValidator;
+import org.apache.isis.metamodel.specloader.validator.MetaModelValidatorVisiting;
+
+import lombok.NonNull;
 
 public final class ProgrammingModelFacetsJava8 extends ProgrammingModelAbstract {
 
@@ -356,6 +367,17 @@ public final class ProgrammingModelFacetsJava8 extends ProgrammingModelAbstract
         addPostProcessor(PostProcessingOrder.A1_BUILTIN, DeriveFacetsPostProcessor.class);
         addValidator(new TitlesAndTranslationsValidator());
 
+        addValidator((objectSpec, validator) -> {
+            final long numActions = objectSpec.streamObjectActions(Contributed.INCLUDED).count();
+            if (numActions > 0) {
+                validator.onFailure(objectSpec, objectSpec.getIdentifier(),
+                        "%s: is a (concrete) but UNKNOWN sort, yet has %d actions",
+                        objectSpec.getCorrespondingClass().getName(),
+                        numActions);
+            }
+            return false;
+        }, objectSpec -> objectSpec.getBeanSort() == BeanSort.UNKNOWN && ! objectSpec.isAbstract());
+
     }
 
 
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/spec/ObjectSpecification.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/spec/ObjectSpecification.java
index 3bb1afc..8c3c24b 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/spec/ObjectSpecification.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/spec/ObjectSpecification.java
@@ -434,6 +434,7 @@ ObjectAssociationContainer, Hierarchical,  DefaultProvider {
     /**
      * Introspecting up to the level required.
      * @since 2.0
+     * @return whether it's necessary to re-run validations.
      */
     void introspectUpTo(IntrospectionState upTo);
 
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoader.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoader.java
index a409bfa..a5b132b 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoader.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoader.java
@@ -133,8 +133,5 @@ public interface SpecificationLoader {
     }
 
 
-
-
-
-
+    void revalidateIfNecessary();
 }
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoaderDefault.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoaderDefault.java
index da510c2..6a743d9 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoaderDefault.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoaderDefault.java
@@ -43,6 +43,7 @@ import org.apache.isis.commons.internal.base._Lazy;
 import org.apache.isis.commons.internal.base._Timing;
 import org.apache.isis.commons.internal.collections._Lists;
 import org.apache.isis.commons.internal.environment.IsisSystemEnvironment;
+import org.apache.isis.commons.internal.exceptions._Exceptions;
 import org.apache.isis.config.IsisConfiguration;
 import org.apache.isis.config.beans.IsisBeanTypeRegistry;
 import org.apache.isis.config.beans.IsisBeanTypeRegistryHolder;
@@ -110,6 +111,12 @@ public class SpecificationLoaderDefault implements SpecificationLoader {
     private final SpecificationCacheDefault<ObjectSpecification> cache = 
             new SpecificationCacheDefault<>();
 
+    /**
+     * We only ever mark the metamodel as fully introspected if in {@link #isFullIntrospect() full} introspection mode.
+     */
+    @Getter
+    private boolean metamodelFullyIntrospected = false;
+
     /** JUnit Test Support */
     public static SpecificationLoaderDefault getInstance (
             IsisConfiguration isisConfiguration,
@@ -235,6 +242,9 @@ public class SpecificationLoaderDefault implements SpecificationLoader {
         stopWatch.stop();
         log.info("Metamodel created in " + (long)stopWatch.getMillis() + " ms.");
 
+        if(isFullIntrospect()) {
+            metamodelFullyIntrospected = true;
+        }
     }
     
     @Override
@@ -322,9 +332,25 @@ public class SpecificationLoaderDefault implements SpecificationLoader {
         synchronized (cache) {
             cachedSpec = cache.computeIfAbsent(typeName, __->createSpecification(substitutedType));
         }
-        
+
         cachedSpec.introspectUpTo(upTo);
-        return cachedSpec; 
+
+        return cachedSpec;
+    }
+
+    public void revalidateIfNecessary() {
+        if(!this.isisConfiguration.getReflector().getIntrospector().isValidateIncrementally()) {
+            return;
+        }
+
+        if (this.validationResult.isMemoized()) {
+            this.validationResult.clear();
+        }
+        final ValidationFailures validationFailures = this.getValidationResult();
+
+        if(validationFailures.hasFailures()) {
+            throw _Exceptions.illegalState(String.join("\n", validationFailures.getMessages("[%d] %s")));
+        }
     }
 
     // -- LOOKUP
@@ -368,8 +394,17 @@ public class SpecificationLoaderDefault implements SpecificationLoader {
      * Creates the appropriate type of {@link ObjectSpecification}.
      */
     private ObjectSpecification createSpecification(final Class<?> cls) {
-        
-         // ... and create the specs
+
+        if(isMetamodelFullyIntrospected() && isisConfiguration.getReflector().getIntrospector().isLockAfterFullIntrospection()) {
+            throw _Exceptions.illegalState(
+                    "Cannot introspect class '%s' because the metamodel has been fully introspected and is now locked. " +
+                    "One reason this can happen is if you are attempting to invoke an action through the WrapperFactory " +
+                    "on a service class incorrectly annotated with Spring's @Service annotation instead of " +
+                    "@DomainService.",
+                    cls.getName());
+        }
+
+        // ... and create the specs
         final ObjectSpecificationAbstract objectSpec;
         if (FreeStandingList.class.isAssignableFrom(cls)) {
 
@@ -381,14 +416,13 @@ public class SpecificationLoaderDefault implements SpecificationLoader {
             val typeRegistry = isisBeanTypeRegistryHolder.getIsisBeanTypeRegistry();
 
             val managedBeanNameIfAny = typeRegistry.getManagedBeanNameForType(cls);
-
             objectSpec = new ObjectSpecificationDefault(
-                    cls,
-                    metaModelContext,
-                    facetProcessor, 
-                    managedBeanNameIfAny, 
-                    postProcessor, 
-                    classSubstitutor);
+                            cls,
+                            metaModelContext,
+                            facetProcessor,
+                            managedBeanNameIfAny.orElse(null),
+                            postProcessor,
+                            classSubstitutor);
         }
 
         return objectSpec;
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
index c9f8129..bad3057 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
@@ -275,7 +275,9 @@ public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implem
         if(log.isDebugEnabled()) {
             log.debug("introspectingUpTo: {}, {}", getFullIdentifier(), upTo);
         }
-        
+
+        boolean revalidate = false;
+
         switch (introspectionState) {
         case NOT_INTROSPECTED:
             if(isLessThan(upTo)) {
@@ -289,6 +291,7 @@ public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implem
                 this.introspectionState = IntrospectionState.MEMBERS_BEING_INTROSPECTED;
                 introspectMembers();
                 this.introspectionState = IntrospectionState.TYPE_AND_MEMBERS_INTROSPECTED;
+                revalidate = true;
             }
             // set to avoid infinite loops
             break;
@@ -301,6 +304,7 @@ public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implem
                 this.introspectionState = IntrospectionState.MEMBERS_BEING_INTROSPECTED;
                 introspectMembers();
                 this.introspectionState = IntrospectionState.TYPE_AND_MEMBERS_INTROSPECTED;
+                revalidate = true;
             }
             break;
         case MEMBERS_BEING_INTROSPECTED:
@@ -308,6 +312,13 @@ public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implem
         case TYPE_AND_MEMBERS_INTROSPECTED:
             // nothing to do
             break;
+
+        default:
+            throw _Exceptions.unexpectedCodeReach();
+        }
+
+        if(revalidate) {
+            getSpecificationLoader().revalidateIfNecessary();
         }
     }
 
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java
index 66b4120..14b38aa 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java
@@ -33,6 +33,8 @@ import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.base._Lazy;
 import org.apache.isis.commons.internal.collections._Lists;
 import org.apache.isis.commons.internal.collections._Maps;
+import org.apache.isis.commons.internal.exceptions._Exceptions;
+import org.apache.isis.commons.internal.ioc.BeanSort;
 import org.apache.isis.metamodel.commons.StringExtensions;
 import org.apache.isis.metamodel.commons.ToString;
 import org.apache.isis.metamodel.context.MetaModelContext;
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/validator/MetaModelValidatorVisiting.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/validator/MetaModelValidatorVisiting.java
index e65f7f6..2494719 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/validator/MetaModelValidatorVisiting.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/validator/MetaModelValidatorVisiting.java
@@ -19,14 +19,14 @@
 
 package org.apache.isis.metamodel.specloader.validator;
 
+import java.util.function.Predicate;
+
 import org.apache.isis.metamodel.spec.ObjectSpecification;
 import org.apache.isis.metamodel.specloader.SpecificationLoaderDefault;
 
 import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
 import lombok.val;
 
-@RequiredArgsConstructor(staticName = "of")
 public class MetaModelValidatorVisiting extends MetaModelValidatorAbstract {
 
     // -- INTERFACES
@@ -44,9 +44,37 @@ public class MetaModelValidatorVisiting extends MetaModelValidatorAbstract {
     }
     
     // -- IMPLEMENTATION
-    
+
+    public static MetaModelValidatorVisiting of(
+            final @NonNull Visitor visitor) {
+        return new MetaModelValidatorVisiting(visitor);
+    }
+
+    public static MetaModelValidatorVisiting of(
+            final @NonNull Visitor visitor,
+            final @NonNull Predicate<ObjectSpecification> includeIf) {
+        return new MetaModelValidatorVisiting(visitor, includeIf);
+    }
+
+
     @NonNull private final Visitor visitor;
-    
+    @NonNull private final Predicate<ObjectSpecification> includeIf;
+
+    private MetaModelValidatorVisiting(
+            final @NonNull Visitor visitor,
+            final @NonNull Predicate<ObjectSpecification> includeIf) {
+        this.visitor = visitor;
+        this.includeIf = includeIf;
+    }
+
+    private MetaModelValidatorVisiting(
+            final @NonNull Visitor visitor) {
+        this(visitor,
+                // by default, exclude managed beans from validation
+                spec -> !spec.isManagedBean() && !spec.getBeanSort().isUnknown()
+        );
+    }
+
     @Override
     public void collectFailuresInto(@NonNull ValidationFailures validationFailures) {
         validateAll();
@@ -59,9 +87,9 @@ public class MetaModelValidatorVisiting extends MetaModelValidatorAbstract {
         val specLoader = (SpecificationLoaderDefault)super.getMetaModelContext().getSpecificationLoader();
         
         specLoader.forEach(spec->{
-            
-            if(spec.isManagedBean() || spec.getBeanSort().isUnknown()) {
-                return; // exclude managed beans from validation
+
+            if(! includeIf.test(spec)) {
+                return;
             }
             
             visitor.visit(spec, this);            
@@ -69,6 +97,10 @@ public class MetaModelValidatorVisiting extends MetaModelValidatorAbstract {
         
     }
 
+    private static boolean filter(ObjectSpecification spec) {
+        return spec.isManagedBean() || spec.getBeanSort().isUnknown();
+    }
+
     private void summarize() {
         if(visitor instanceof SummarizingVisitor) {
             SummarizingVisitor summarizingVisitor = (SummarizingVisitor) visitor;
diff --git a/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/metamodel/JdoProgrammingModelPlugin.java b/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/metamodel/JdoProgrammingModelPlugin.java
index 32dc1d9..0549ee8 100644
--- a/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/metamodel/JdoProgrammingModelPlugin.java
+++ b/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/metamodel/JdoProgrammingModelPlugin.java
@@ -205,6 +205,8 @@ public class JdoProgrammingModelPlugin implements MetaModelRefiner {
                         
                     }
                 }
+                // so can be revalidated again if necessary.
+                collidingSpecsById.clear();
             }
 
             private String asCsv(final List<ObjectSpecification> specList) {
diff --git a/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/IsisTransactionJdo.java b/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/IsisTransactionJdo.java
index 550cb40..f5de05a 100644
--- a/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/IsisTransactionJdo.java
+++ b/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/IsisTransactionJdo.java
@@ -44,6 +44,7 @@ import org.apache.isis.runtime.persistence.transaction.IsisTransactionFlushExcep
 import org.apache.isis.runtime.persistence.transaction.IsisTransactionManagerException;
 
 import lombok.Getter;
+import lombok.Setter;
 import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
@@ -183,12 +184,8 @@ public class IsisTransactionJdo implements Transaction {
 
     // -- state
 
+    @Getter
     private State state;
-
-    public State getState() {
-        return state;
-    }
-
     private void setState(final State state) {
         this.state = state;
         if(state.isComplete()) {
diff --git a/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/PersistenceSession5.java b/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/PersistenceSession5.java
index 20dfc98..45d0d56 100644
--- a/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/PersistenceSession5.java
+++ b/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/PersistenceSession5.java
@@ -172,7 +172,7 @@ implements IsisLifecycleListener.PersistenceSessionLifecycleManagement {
         }
 
         final Command command = createCommand();
-        final Interaction interaction = factoryService.instantiate(Interaction.class);
+        final Interaction interaction = new Interaction();
 
         final Timestamp timestamp = clockService.nowAsJavaSqlTimestamp();
         final String userName = userService.getUser().getName();