diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/factory/FactoryService.java b/api/applib/src/main/java/org/apache/causeway/applib/services/factory/FactoryService.java index 92560c33059..f767a566cfb 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/services/factory/FactoryService.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/services/factory/FactoryService.java @@ -20,15 +20,13 @@ import java.util.NoSuchElementException; -import org.jspecify.annotations.Nullable; - import org.apache.causeway.applib.annotation.CollectionLayout; import org.apache.causeway.applib.annotation.PropertyLayout; import org.apache.causeway.applib.exceptions.UnrecoverableException; import org.apache.causeway.applib.graph.tree.TreeNode; import org.apache.causeway.applib.services.bookmark.Bookmark; - import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** * Collects together methods for instantiating domain objects, also injecting @@ -109,7 +107,7 @@ public interface FactoryService { * @param * @param mixinClass * @param mixedIn - * @throws IllegalArgumentException if mixinClass is not a mixin type + * @throws IllegalArgumentException if mixinClass is not a mixin type or if mixinClass is not already part of the metamodel * @apiNote forces the mixinClass to be added to the meta-model if not already */ T mixin(@NonNull Class mixinClass, @NonNull Object mixedIn); diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/collections/_Multimaps.java b/commons/src/main/java/org/apache/causeway/commons/internal/collections/_Multimaps.java index 941e059b6ee..56d6ab0a9f9 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/collections/_Multimaps.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/collections/_Multimaps.java @@ -40,11 +40,10 @@ import java.util.function.Supplier; import java.util.stream.Stream; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - import org.apache.causeway.commons.internal.base._Casts; import org.apache.causeway.commons.internal.exceptions._Exceptions; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** *

- internal use only -

@@ -66,6 +65,7 @@ public class _Multimaps { * @param */ public static interface ListMultimap extends Map> { + /** * Adds {@code value} to the List stored under {@code key}. * (If no such List exists, a new List is created.) diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/concurrent/_ConcurrentTaskList.java b/commons/src/main/java/org/apache/causeway/commons/internal/concurrent/_ConcurrentTaskList.java index 2bf24c0cc5a..37cddb77784 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/concurrent/_ConcurrentTaskList.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/concurrent/_ConcurrentTaskList.java @@ -36,6 +36,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor(staticName = "named") @@ -168,7 +169,6 @@ public Duration getExecutionTime() { // -- EXECUTION LOGGING private void onFinished(final _ConcurrentContext context) { - for(var task: tasks) { if(task.getFailedWith()!=null) { log.error("----------------------------------------"); @@ -178,9 +178,8 @@ private void onFinished(final _ConcurrentContext context) { } } - if(!context.enableExecutionLogging()) { - return; - } + if(!context.enableExecutionLogging()) + return; log.info("TaskList '%s' running %d/%d tasks %s, took %.3f milliseconds " .formatted(getName(), @@ -191,6 +190,15 @@ private void onFinished(final _ConcurrentContext context) { } + @SneakyThrows + public void rethrow() { + for(var task: tasks) { + if(task.getFailedWith()!=null) { + throw task.getFailedWith(); + } + } + } + // -- SHORTCUTS public _ConcurrentTaskList addRunnable(final String name, final Runnable runnable) { diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/debug/_Debug.java b/commons/src/main/java/org/apache/causeway/commons/internal/debug/_Debug.java index b5195ed3903..1762cc7a6a5 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/debug/_Debug.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/debug/_Debug.java @@ -18,11 +18,18 @@ */ package org.apache.causeway.commons.internal.debug; +import java.util.LongSummaryStatistics; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.causeway.commons.internal.base._NullSafe; import org.apache.causeway.commons.internal.base._Strings; +import org.apache.causeway.commons.internal.context._Context; import org.apache.causeway.commons.internal.debug.xray.XrayUi; +import org.jspecify.annotations.NonNull; import lombok.experimental.UtilityClass; @@ -46,7 +53,8 @@ @UtilityClass public class _Debug { - public void onCondition( + @Deprecated(forRemoval = false) // do not remove, see java-doc + public void onCondition( final boolean condition, final Runnable runnable) { @@ -55,24 +63,121 @@ public void onCondition( } } - public void onClassSimpleNameMatch( + @Deprecated(forRemoval = false) // do not remove, see java-doc + public void onClassSimpleNameMatch( final Class correspondingClass, final String classSimpleName, final Runnable runnable) { onCondition(correspondingClass.getSimpleName().equals(classSimpleName), runnable); } - public void dump(final Object x) { + @Deprecated(forRemoval = false) // do not remove, see java-doc + public void dump(final Object x) { dump(x, 0); } /** * General purpose log entry. */ - public void log(final String format, final Object...args) { + @Deprecated(forRemoval = false) // do not remove, see java-doc + public void log(final String format, final Object...args) { _XrayEvent.record(1, _IconResource.LOG, format, args); } + public record Profiler( + Map measurements) { + + public record Measurement( + String name, + LongSummaryStatistics stats) { + Measurement(final String name) { + this(name, new LongSummaryStatistics()); + } + void collect(final Runnable runnable) { + var t0 = System.nanoTime(); + runnable.run(); + stats.accept(System.nanoTime() - t0); + } + T collect(final Supplier callable) { + var t0 = System.nanoTime(); + var t = callable.get(); + stats.accept(System.nanoTime() - t0); + return t; + } + @Override + public final String toString() { + return "Profiling %s: %.3f ms, avg %.3f ms (count=%d)" + .formatted(name, + (stats.getSum())/1000_000., + stats.getAverage()/1000_000., + stats.getCount()); + } + } + + public static Profiler getInstance() { + return _Context.computeIfAbsent(Profiler.class, (@NonNull Supplier) Profiler::new); + } + + public Profiler() { + this(new ConcurrentHashMap<>()); + } + + public void measure(final String name, final Runnable runnable) { + measurements.computeIfAbsent(name, Measurement::new) + .collect(runnable); + } + + public T measure(final String name, final Supplier callable) { + return measurements.computeIfAbsent(name, Measurement::new) + .collect(callable); + } + + @Override + public final String toString() { + return new TreeMap<>(measurements).values().stream() + .map(Measurement::toString) + .collect(Collectors.joining("\n")); + } + } + + @Deprecated(forRemoval = false) // do not remove, see java-doc + public String measureTimeResolutionNanos() { + final int iterations = 1_000_000; // Run many samples + long zeroChangeCount = 0; + long totalSteps = 0; + + var sb = new StringBuilder(); + + sb.append("Sampling " + iterations + " consecutive calls to System.nanoTime()..."); + + final LongSummaryStatistics stats = new LongSummaryStatistics(); + + for (int i = 0; i < iterations; i++) { + long delta = System.nanoTime() - System.nanoTime(); + if (delta == 0) { + zeroChangeCount++; + } else { + stats.accept(-delta); + } + } + + double percentageSame = (double) zeroChangeCount / iterations * 100; + + sb.append("\n--- Results ---"); + sb.append("\nTotal Iterations: " + iterations); + sb.append("\nTimes resolution didn't change: " + zeroChangeCount); + sb.append("\nPercentage of identical consecutive readings: " + String.format("%.2f", percentageSame) + "%"); + sb.append("\nTotal unique 'ticks' detected: " + totalSteps); + + sb.append("\n\n--- Delta Statistics (Time between calls) ---"); + sb.append("\nTotal valid deltas recorded: " + stats.getCount()); + sb.append("\nMinimum delta detected: " + stats.getMin() + " ns"); + sb.append("\nMaximum delta detected: " + stats.getMax() + " ns"); + sb.append("\nAverage delta: %.2f ns\n".formatted(stats.getAverage())); + + return sb.toString(); + } + // -- HELPER private void dump(Object x, final int indent) { diff --git a/core/config/src/main/java/org/apache/causeway/core/config/beans/CausewayBeanMetaData.java b/core/config/src/main/java/org/apache/causeway/core/config/beans/CausewayBeanMetaData.java index c2bc062be6b..65a30bd3902 100644 --- a/core/config/src/main/java/org/apache/causeway/core/config/beans/CausewayBeanMetaData.java +++ b/core/config/src/main/java/org/apache/causeway/core/config/beans/CausewayBeanMetaData.java @@ -20,12 +20,13 @@ import java.io.Serializable; +import org.jspecify.annotations.NonNull; + import org.apache.causeway.applib.id.LogicalType; import org.apache.causeway.applib.services.metamodel.BeanSort; +import org.apache.causeway.applib.services.registry.ServiceRegistry; import org.apache.causeway.commons.internal.base._Strings; -import org.jspecify.annotations.NonNull; - public record CausewayBeanMetaData( @NonNull LogicalType logicalType, @NonNull BeanSort beanSort, @@ -86,6 +87,23 @@ public String getBeanName() { return logicalType.logicalName(); } + public boolean isInjectable(final ServiceRegistry serviceRegistry) { + // optimization + final boolean isDefinitelyNotInjectible = switch (managedBy()) { + case NONE, CAUSEWAY, PERSISTENCE -> true; + case UNSPECIFIED, SPRING -> beanSort.isAbstract() + || beanSort.isValue() + || beanSort.isEntity() + || beanSort.isViewModel() + || beanSort.isMixin(); + }; + return !isDefinitelyNotInjectible + && (beanSort.isManagedBeanAny() + || serviceRegistry + .lookupRegisteredBeanById(logicalType()) + .isPresent()); + } + // -- FACTORIES public static CausewayBeanMetaData vetoed( diff --git a/core/config/src/main/java/org/apache/causeway/core/config/beans/CausewayBeanTypeRegistry.java b/core/config/src/main/java/org/apache/causeway/core/config/beans/CausewayBeanTypeRegistry.java index 3386685b816..021fb4d5d71 100644 --- a/core/config/src/main/java/org/apache/causeway/core/config/beans/CausewayBeanTypeRegistry.java +++ b/core/config/src/main/java/org/apache/causeway/core/config/beans/CausewayBeanTypeRegistry.java @@ -23,15 +23,13 @@ import java.util.Optional; import java.util.stream.Stream; -import org.jspecify.annotations.Nullable; - import org.apache.causeway.applib.annotation.HomePage; import org.apache.causeway.applib.annotation.Programmatic; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.internal.reflection._ClassCache; import org.apache.causeway.core.config.beans.CausewayBeanMetaData.PersistenceStack; - import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** * Holds discovered domain types grouped by bean-sort. diff --git a/core/config/src/main/java/org/apache/causeway/core/config/environment/CausewaySystemEnvironment.java b/core/config/src/main/java/org/apache/causeway/core/config/environment/CausewaySystemEnvironment.java index 9aff22a04ea..58c6c5fd553 100644 --- a/core/config/src/main/java/org/apache/causeway/core/config/environment/CausewaySystemEnvironment.java +++ b/core/config/src/main/java/org/apache/causeway/core/config/environment/CausewaySystemEnvironment.java @@ -18,10 +18,11 @@ */ package org.apache.causeway.core.config.environment; -import jakarta.annotation.PreDestroy; -import jakarta.annotation.Priority; -import jakarta.inject.Named; - +import org.apache.causeway.commons.internal.base._StableValue; +import org.apache.causeway.commons.internal.base._Strings; +import org.apache.causeway.commons.internal.context._Context; +import org.apache.causeway.commons.internal.ioc.SpringContextHolder; +import org.apache.causeway.core.config.CausewayModuleCoreConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.event.ApplicationFailedEvent; @@ -31,12 +32,9 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; -import org.apache.causeway.commons.internal.base._StableValue; -import org.apache.causeway.commons.internal.base._Strings; -import org.apache.causeway.commons.internal.context._Context; -import org.apache.causeway.commons.internal.ioc.SpringContextHolder; -import org.apache.causeway.core.config.CausewayModuleCoreConfig; - +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Priority; +import jakarta.inject.Named; import lombok.Getter; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; @@ -63,10 +61,10 @@ public class CausewaySystemEnvironment { private final DeploymentType deploymentType; @Autowired - public CausewaySystemEnvironment(ApplicationContext springContext) { + public CausewaySystemEnvironment(final ApplicationContext springContext) { this.springContextHolder = new SpringContextHolder(springContext); this.deploymentType = deploymentTypeFromEnvironment(); - log.info("init for %s (hashCode = {})", deploymentType, this.hashCode()); + log.info("init for {} (hashCode = {})", deploymentType, this.hashCode()); } //JUnit @@ -184,7 +182,7 @@ private static boolean isNotSet(final String value) { return "false".equalsIgnoreCase(value); } - private _StableValue _isIntegrationTesting = new _StableValue(); + private final _StableValue _isIntegrationTesting = new _StableValue(); /** * Whether we find Spring's ContextCache on the class path. */ diff --git a/core/config/src/main/java/org/apache/causeway/core/config/validators/PatternOptionalStringConstraintValidator.java b/core/config/src/main/java/org/apache/causeway/core/config/validators/PatternOptionalStringConstraintValidator.java index dd1415053eb..1bbb4dfcdc8 100644 --- a/core/config/src/main/java/org/apache/causeway/core/config/validators/PatternOptionalStringConstraintValidator.java +++ b/core/config/src/main/java/org/apache/causeway/core/config/validators/PatternOptionalStringConstraintValidator.java @@ -19,6 +19,7 @@ package org.apache.causeway.core.config.validators; import java.util.Optional; +import java.util.regex.Matcher; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; @@ -26,36 +27,39 @@ import org.springframework.stereotype.Component; -import org.apache.causeway.commons.internal.context._Context; - -import lombok.SneakyThrows; - @Component public class PatternOptionalStringConstraintValidator -implements ConstraintValidator> { + implements ConstraintValidator> { - private final ConstraintValidator patternValidator; - - @SneakyThrows - public PatternOptionalStringConstraintValidator(){ - var patternValidatorClass = _Context.loadClass("org.hibernate.validator.internal.constraintvalidators.bv.PatternValidator"); - this.patternValidator = (ConstraintValidator) - patternValidatorClass - .getConstructor() - .newInstance(); - } + private java.util.regex.Pattern regex; + private String regexp; + private int flags; @Override - public void initialize(final jakarta.validation.constraints.Pattern constraintAnnotation) { - patternValidator.initialize(constraintAnnotation); + public void initialize(final Pattern annotation) { + this.regexp = annotation.regexp(); + this.flags = mapFlags(annotation.flags()); + this.regex = java.util.regex.Pattern.compile(this.regexp, this.flags); } @Override - public boolean isValid( - final Optional value, - final ConstraintValidatorContext context) { - if(!value.isPresent()) return true; + public boolean isValid(final Optional value, final ConstraintValidatorContext context) { + if (value == null || value.isEmpty()) + return true; + + String s = value.get(); + + // Match semantics for Bean Validation @Pattern is "find a match", not "full match" + // If you want full match, use matcher.matches() and/or add ^...$ in regexp. + Matcher m = regex.matcher(s); + return m.find(); + } - return patternValidator.isValid(value.get(), context); + private static int mapFlags(final jakarta.validation.constraints.Pattern.Flag[] flags) { + int out = 0; + for (jakarta.validation.constraints.Pattern.Flag f : flags) { + out |= f.getValue(); + } + return out; } -} +} \ 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 f081c5fbb41..eae443b7112 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 @@ -22,16 +22,6 @@ import java.util.Objects; import java.util.stream.Stream; -import jakarta.inject.Provider; - -import org.jspecify.annotations.NonNull; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Primary; - import org.apache.causeway.applib.CausewayModuleApplib; import org.apache.causeway.applib.graph.tree.TreeAdapter; import org.apache.causeway.applib.layout.resource.LayoutResourceLoader; @@ -118,6 +108,14 @@ import org.apache.causeway.core.metamodel.valuesemantics.temporal.legacy.JavaUtilDateValueSemantics; import org.apache.causeway.core.metamodel.valuetypes.ValueSemanticsResolverDefault; import org.apache.causeway.core.security.CausewayModuleCoreSecurity; +import org.jspecify.annotations.NonNull; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; + +import jakarta.inject.Provider; @Configuration(proxyBeanMethods = false) @Import({ diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/commons/MetaModelVisitor.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/commons/MetaModelVisitor.java index b9805eacd37..713fea929e1 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/commons/MetaModelVisitor.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/commons/MetaModelVisitor.java @@ -53,7 +53,7 @@ default boolean isEnabled() { */ public final static Predicate SKIP_ABSTRACT = spec->!spec.isAbstract() - && spec.getBeanSort().policy().isIntrospectionAllowed(); + && spec.beanSort().policy().isIntrospectionAllowed(); /** types pass this filter, if is NOT a mixin */ public final static Predicate SKIP_MIXINS = @@ -61,7 +61,7 @@ default boolean isEnabled() { /** types pass this filter, if IS a mixin */ public final static Predicate MIXINS = - spec->spec.isMixin(); + ObjectSpecification::isMixin; /** types pass this filter, if member-annotation is not required */ public final static Predicate SKIP_WHEN_MEMBER_ANNOT_REQUIRED = diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/HasFacetHolder.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/HasFacetHolder.java index 3b4c68dafd7..3ab7f675d5c 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/HasFacetHolder.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/HasFacetHolder.java @@ -23,6 +23,7 @@ import org.apache.causeway.applib.Identifier; +@FunctionalInterface public interface HasFacetHolder extends FacetHolder { // -- INTERFACE diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelValidator.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelValidator.java index f471aed3413..ed7ae4b20d3 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelValidator.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelValidator.java @@ -37,7 +37,7 @@ public class ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelV @Inject public ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelValidator(final MetaModelContext mmc) { - super(mmc, spec->spec.getBeanSort() == BeanSort.UNKNOWN + super(mmc, spec->spec.beanSort() == BeanSort.UNKNOWN && !spec.isAbstract()); } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionOverloadingValidator.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionOverloadingValidator.java index 1e60eba62af..8d96381f1f5 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionOverloadingValidator.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionOverloadingValidator.java @@ -46,7 +46,7 @@ public class ActionOverloadingValidator @Inject public ActionOverloadingValidator(final MetaModelContext mmc) { - super(mmc, spec->spec.getBeanSort()!=BeanSort.UNKNOWN + super(mmc, spec->spec.beanSort()!=BeanSort.UNKNOWN && !spec.isAbstract()); } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/described/annotprop/MemberDescribedFacetFromType.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/described/annotprop/MemberDescribedFacetFromType.java index 7c233a1694e..19fea810627 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/described/annotprop/MemberDescribedFacetFromType.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/described/annotprop/MemberDescribedFacetFromType.java @@ -32,13 +32,9 @@ public class MemberDescribedFacetFromType public static Optional create( final ObjectDescribedFacet objectDescribedFacet, final FacetHolder holder) { - - var describedIfAny = _Strings.emptyToNull(objectDescribedFacet.text()); - - return Optional.ofNullable(describedIfAny) - .map(described-> - new MemberDescribedFacetFromType(described, holder)); - + return _Strings.nonEmpty(objectDescribedFacet.text()) + .map(described-> + new MemberDescribedFacetFromType(described, holder)); } private MemberDescribedFacetFromType( diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java index fd7df048cf4..3a2924a07c6 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java @@ -592,7 +592,7 @@ public void validateObjectEnter(final ObjectSpecification objSpec) { specsByLogicalTypeName.putElement(objSpec.logicalTypeName(), objSpec); // also adding aliases to the multi-map - objSpec.getAliases() + objSpec.aliases() .forEach(alias-> specsByLogicalTypeName.putElement(alias.logicalName(), objSpec)); } @@ -649,7 +649,7 @@ private Can proxiesIn(final @Nullable List objectSpecification.logicalTypeName().equals(name)); } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/ignore/annotation/RemoveAnnotatedMethodsFacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/ignore/annotation/RemoveAnnotatedMethodsFacetFactory.java index 5da343ad2c7..6d40f17c208 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/ignore/annotation/RemoveAnnotatedMethodsFacetFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/ignore/annotation/RemoveAnnotatedMethodsFacetFactory.java @@ -21,10 +21,6 @@ import java.util.function.Consumer; import java.util.function.Predicate; -import jakarta.inject.Inject; - -import org.jspecify.annotations.NonNull; - import org.apache.causeway.commons.internal.functions._Predicates; import org.apache.causeway.commons.internal.reflection._ClassCache.Attribute; import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; @@ -33,6 +29,9 @@ import org.apache.causeway.core.metamodel.facetapi.FeatureType; import org.apache.causeway.core.metamodel.facets.FacetFactoryAbstract; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import org.jspecify.annotations.NonNull; + +import jakarta.inject.Inject; public class RemoveAnnotatedMethodsFacetFactory extends FacetFactoryAbstract { @@ -103,7 +102,7 @@ private Predicate isMixinMainMethod(final @NonNull ProcessClassC // shortcut, when we already know the class is not a mixin if(processClassContext.getFacetHolder() instanceof ObjectSpecification) { var spec = (ObjectSpecification) processClassContext.getFacetHolder(); - if(!spec.getBeanSort().isMixin()) + if(!spec.beanSort().isMixin()) return method->false; } // lookup attribute from class-cache as it should have been already processed by the BeanTypeClassifier diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/ignore/javalang/RemoveMethodsFacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/ignore/javalang/RemoveMethodsFacetFactory.java index c2f380c0aa1..cc30e6e0d22 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/ignore/javalang/RemoveMethodsFacetFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/ignore/javalang/RemoveMethodsFacetFactory.java @@ -18,8 +18,6 @@ */ package org.apache.causeway.core.metamodel.facets.object.ignore.javalang; -import jakarta.inject.Inject; - import org.apache.causeway.applib.annotation.Action; import org.apache.causeway.commons.internal._Constants; import org.apache.causeway.commons.internal.reflection._Annotations; @@ -30,6 +28,8 @@ import org.apache.causeway.core.metamodel.facets.FacetFactoryAbstract; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import jakarta.inject.Inject; + /** * Designed to simply filter out any synthetic methods. * @@ -52,8 +52,8 @@ public void process(final ProcessClassContext processClassContext) { var cls = processClassContext.getCls(); var facetHolder = processClassContext.getFacetHolder(); - var isConcreteMixin = facetHolder instanceof ObjectSpecification - ? ((ObjectSpecification)facetHolder).getBeanSort().isMixin() + var isConcreteMixin = facetHolder instanceof ObjectSpecification o + ? o.beanSort().isMixin() : false; var isActionAnnotationRequired = processClassContext.getIntrospectionPolicy() @@ -99,9 +99,8 @@ public void process(final ProcessClassContext processClassContext) { } private void removeSuperclassMethods(final Class type, final ProcessClassContext processClassContext) { - if (type == null) { - return; - } + if (type == null) + return; if (!_Reflect.isJavaApiClass(type)) { removeSuperclassMethods(type.getSuperclass(), processClassContext); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/logicaltype/classname/LogicalTypeFacetFromClassNameFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/logicaltype/classname/LogicalTypeFacetFromClassNameFactory.java index d77c815aa3e..f452efeca0b 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/logicaltype/classname/LogicalTypeFacetFromClassNameFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/logicaltype/classname/LogicalTypeFacetFromClassNameFactory.java @@ -18,9 +18,6 @@ */ package org.apache.causeway.core.metamodel.facets.object.logicaltype.classname; -import jakarta.inject.Inject; -import jakarta.xml.bind.annotation.XmlType; - import org.apache.causeway.commons.internal.reflection._ClassCache; import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants.MessageTemplate; import org.apache.causeway.core.metamodel.context.MetaModelContext; @@ -34,6 +31,9 @@ import org.apache.causeway.core.metamodel.spec.feature.MixedIn; import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure; +import jakarta.inject.Inject; +import jakarta.xml.bind.annotation.XmlType; + public class LogicalTypeFacetFromClassNameFactory extends FacetFactoryAbstract implements @@ -78,7 +78,7 @@ public void refineProgrammingModel(final ProgrammingModel programmingModel) { ValidationFailure.raise(objectSpec, MessageTemplate.LOGICAL_TYPE_NAME_IS_NOT_EXPLICIT .builder() .addVariable("type", objectSpec.getFullIdentifier()) - .addVariable("beanSort", objectSpec.getBeanSort().name()) + .addVariable("beanSort", objectSpec.beanSort().name()) .addVariable("configProperty", "causeway.core.meta-model.validator.explicit-logical-type-names") .buildMessage()); } @@ -94,11 +94,10 @@ private boolean skip(final ObjectSpecification objectSpec) { || objectSpec.isMixin() || MmSpecUtils.isFixtureScript(objectSpec)) return true; if (objectSpec.isEntity()) return false; - if (objectSpec.isViewModel()) { - // with + if (objectSpec.isViewModel()) + // with // skip JAXB DTOs return objectSpec.getCorrespondingClass().getAnnotation(XmlType.class) != null; - } if (objectSpec.isInjectable()) { // only check if its a domain service (that is potentially contributing to UI or Web-API(s). if(!objectSpec.isDomainService()) return true; diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/mixin/MixinFacet.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/mixin/MixinFacet.java index 0241127daa6..78a8adf5ded 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/mixin/MixinFacet.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/mixin/MixinFacet.java @@ -43,6 +43,7 @@ public interface MixinFacet extends Facet { public enum Contributing { /** + * FIXME remove * Initial state early during introspection. */ UNSPECIFIED, @@ -65,12 +66,15 @@ public enum Contributing { public boolean isUnspecified() { return this==UNSPECIFIED; } } + Class mixinType(); + Class mixeeType(); + Contributing contributing(); /** * The mixin's main method name. */ - String getMainMethodName(); + String mainMethodName(); boolean isMixinFor(Class candidateDomainType); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/mixin/MixinFacetAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/mixin/MixinFacetAbstract.java index 18086f6c0a6..82a33cef80c 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/mixin/MixinFacetAbstract.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/mixin/MixinFacetAbstract.java @@ -29,9 +29,9 @@ import org.apache.causeway.core.metamodel.facetapi.FacetHolder; import org.apache.causeway.core.metamodel.facets.FacetedMethod; import org.apache.causeway.core.metamodel.facets.actions.contributing.ContributingFacet; +import org.jspecify.annotations.NonNull; import lombok.Getter; -import org.jspecify.annotations.NonNull; import lombok.experimental.Accessors; //@Slf4j @@ -39,13 +39,16 @@ public abstract class MixinFacetAbstract extends FacetAbstract implements MixinFacet { - @Getter(onMethod_={@Override}) + @Getter(onMethod_={@Override}) @Accessors(fluent=true) private final @NonNull String mainMethodName; @Getter(onMethod_={@Override}) @Accessors(fluent=true) private @NonNull Contributing contributing = Contributing.UNSPECIFIED; + @Getter(onMethod_={@Override}) @Accessors(fluent=true) private final @NonNull Class mixinType; - private final @NonNull Class holderType; + + @Getter(onMethod_={@Override}) @Accessors(fluent=true) + private final @NonNull Class mixeeType; private final @NonNull Constructor constructor; private static final Class type() { @@ -63,33 +66,30 @@ protected MixinFacetAbstract( this.mixinType = mixinType; this.constructor = constructor; // by mixin convention: first constructor argument is identified as the holder type - this.holderType = constructor.getParameterTypes()[0]; + this.mixeeType = constructor.getParameterTypes()[0]; } @Override public boolean isMixinFor(final Class candidateDomainType) { return candidateDomainType == null ? false - : holderType.isAssignableFrom(candidateDomainType); + : mixeeType.isAssignableFrom(candidateDomainType); } @Override public Object instantiate(final Object mixee) { - if(constructor == null) { - throw _Exceptions.unrecoverable( + if(constructor == null) + throw _Exceptions.unrecoverable( "Failed to instantiate mixin. " + "Invalid mix-in declaration of type %s, missing contructor", mixinType); - } - if(mixee == null) { - return null; - } - if(!isMixinFor(mixee.getClass())) { - throw _Exceptions.illegalArgument( + if(mixee == null) + return null; + if(!isMixinFor(mixee.getClass())) + throw _Exceptions.illegalArgument( "Failed to instantiate mixin. " + "Mixin class %s is not a mixin for supplied object [%s]. " + "Mixin construction expects type: %s", - mixinType.getName(), mixee, holderType); - } + mixinType.getName(), mixee, mixeeType); try { var mixinPojo = constructor.newInstance(mixee); getServiceInjector().injectServicesInto(mixinPojo); @@ -110,7 +110,7 @@ public boolean isCandidateForMain(final ResolvedMethod method) { * mixin invocation will take care of calling the right method, * that is in terms of type-hierarchy the 'nearest' to this mixin; */ - return method.name().equals(getMainMethodName()) + return method.name().equals(mainMethodName()) && method.method().getDeclaringClass() .isAssignableFrom(constructor.getDeclaringClass()); } @@ -121,7 +121,7 @@ public void visitAttributes(final BiConsumer visitor) { visitor.accept("mixinType", mixinType); visitor.accept("contributing", contributing); visitor.accept("mainMethodName", mainMethodName); - visitor.accept("holderType", holderType); + visitor.accept("mixeeType", mixeeType); } /** diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java index 99185f20688..adec4ca31a7 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java @@ -20,8 +20,6 @@ import java.util.Objects; -import jakarta.inject.Inject; - import org.apache.causeway.applib.services.bookmark.HmacAuthority; import org.apache.causeway.applib.services.jaxb.JaxbService; import org.apache.causeway.applib.services.urlencoding.UrlEncodingService; @@ -38,6 +36,8 @@ import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext; import org.apache.causeway.core.metamodel.valuesemantics.ValueCodec; +import jakarta.inject.Inject; + public class ViewModelFacetFactory extends FacetFactoryAbstract implements @@ -109,7 +109,7 @@ public void refineProgrammingModel(final ProgrammingModel programmingModel) { // ensure concrete viewmodel types have a ViewModelFacet if(!objectSpec.isAbstract() - && objectSpec.getBeanSort().isViewModel() + && objectSpec.beanSort().isViewModel() && !objectSpec.viewmodelFacet().isPresent()) { ValidationFailure.raiseFormatted(objectSpec, ProgrammingModelConstants.MessageTemplate.VIEWMODEL_MISSING_SERIALIZATION_STRATEGY diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java index 65d2e8509a3..392d70249b4 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java @@ -22,8 +22,6 @@ import java.util.Optional; import java.util.stream.Stream; -import org.jspecify.annotations.NonNull; - import org.apache.causeway.applib.annotation.DomainObject; import org.apache.causeway.applib.services.metamodel.BeanSort; import org.apache.causeway.commons.internal.base._Casts; @@ -36,6 +34,7 @@ import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; import org.apache.causeway.core.metamodel.util.hmac.Memento; import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext; +import org.jspecify.annotations.NonNull; public final class ViewModelFacetForDomainObjectAnnotation extends SecureViewModelFacet { @@ -60,14 +59,13 @@ public static Optional create( //[CAUSEWAY-3068] consider what the BeanTypeClassifier has come up with final boolean isClassifiedAsViewModel = _Casts.castTo(ObjectSpecification.class, holder) - .map(ObjectSpecification::getBeanSort) + .map(ObjectSpecification::beanSort) .map(BeanSort::isViewModel) .orElse(false); - if(!isClassifiedAsViewModel) { - // not a ViewModel, so no ViewModelFacet + if(!isClassifiedAsViewModel) + // not a ViewModel, so no ViewModelFacet return null; - } // else fall through case VIEW_MODEL: return new ViewModelFacetForDomainObjectAnnotation(mementoContext, holder); @@ -152,7 +150,7 @@ private Stream streamPersistableProperties( // ignore read-only .filter(property->property.containsNonFallbackFacet(PropertySetterFacet.class)) // ignore those explicitly annotated as @Property(snapshot = Snapshot.EXCLUDED) - .filter(property->property.isIncludedWithSnapshots()); + .filter(OneToOneAssociation::isIncludedWithSnapshots); } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/inspect/model/TypeNode.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/inspect/model/TypeNode.java index ac6af34d3c4..76ab934f15b 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/inspect/model/TypeNode.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/inspect/model/TypeNode.java @@ -60,7 +60,7 @@ public String iconName() { public void putDetails(final Details details) { var spec = spec().orElse(null); if(spec==null) return; - details.put("Bean Sort", spec.getBeanSort().name()); + details.put("Bean Sort", spec.beanSort().name()); details.put("Simple Name", spec.logicalType().logicalSimpleName()); details.put("Namespace", spec.logicalType().namespace()); details.put("Corresponding Class", spec.getCorrespondingClass().getName()); @@ -72,7 +72,7 @@ public void putDetails(final Details details) { .forEach(interfc->details.put( "Interface", interfc.getCorrespondingClass().getName())); - switch (spec.getBeanSort()) { + switch (spec.beanSort()) { case ENTITY, ABSTRACT -> { if(!spec.getCorrespondingClass().isInterface()) { var classCache = _ClassCache.getInstance(); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/InteractionHead.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/InteractionHead.java index 3aadd74c747..f53a24b3f85 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/InteractionHead.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/InteractionHead.java @@ -18,9 +18,6 @@ */ package org.apache.causeway.core.metamodel.interactions; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - import org.apache.causeway.applib.services.command.Command; import org.apache.causeway.commons.internal.exceptions._Exceptions; import org.apache.causeway.core.metamodel.object.ManagedObject; @@ -29,6 +26,8 @@ import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** * Model that holds the objects involved with the interaction. @@ -47,9 +46,9 @@ public record InteractionHead( * where {@code target} is the mixin instance. */ ManagedObject target) { - + // -- FACTORIES - + /** Regular case, when owner equals target. (no mixin) */ public static InteractionHead regular(final ManagedObject owner) { return new InteractionHead(owner, owner); @@ -59,20 +58,18 @@ public static InteractionHead regular(final ManagedObject owner) { public static InteractionHead mixin(final @NonNull ManagedObject owner, final @NonNull ManagedObject target) { return new InteractionHead(owner, target); } - + // canonical constructor with consistency checks public InteractionHead( final ManagedObject owner, final ManagedObject target) { if(ManagedObjects.isSpecified(owner) - && owner.objSpec().getBeanSort().isMixin()) { - throw _Exceptions.unrecoverable("unexpected: owner is a mixin %s", owner); - } + && owner.objSpec().beanSort().isMixin()) + throw _Exceptions.unrecoverable("unexpected: owner is a mixin %s", owner); if(ManagedObjects.isSpecified(target) - && target.objSpec().getBeanSort().isMixin() - && target.getPojo()==null) { - throw _Exceptions.unrecoverable("target not spec. %s", target); - } + && target.objSpec().beanSort().isMixin() + && target.getPojo()==null) + throw _Exceptions.unrecoverable("target not spec. %s", target); this.owner = owner; this.target = target; } @@ -89,35 +86,32 @@ public boolean isCommandForMember( && logicalMemberIdentifierFor(objectMember) .equals(command.getLogicalMemberIdentifier()); } - + public String logicalMemberIdentifierFor(final ObjectMember objectMember) { if (!objectMember.isMixedIn() && objectMember instanceof ObjectAction objectAction - && objectAction.isDeclaredOnMixin()) { - // corner case when the objectMember is an ObjectActionDefault but corresponds to a mixin main - return logicalMemberIdentifierFor(owner().objSpec(), + && objectAction.isDeclaredOnMixin()) + // corner case when the objectMember is an ObjectActionDefault but corresponds to a mixin main + return logicalMemberIdentifierFor(owner().objSpec(), objectMember.getProgrammingModel() .mixinNamingStrategy() .memberId(objectAction.getFeatureIdentifier().logicalType().correspondingClass())); - } - if(objectMember instanceof ObjectAction act) { - return logicalMemberIdentifierFor(act.getDeclaringType(), act.getFeatureIdentifier().memberLogicalName()); - } - if(objectMember instanceof OneToOneAssociation prop) { - return logicalMemberIdentifierFor(prop.getDeclaringType(), prop.getFeatureIdentifier().memberLogicalName()); - } - throw new IllegalArgumentException(objectMember.getClass() + " is not supported"); + if(objectMember instanceof ObjectAction act) + return logicalMemberIdentifierFor(act.getDeclaringType(), act.getFeatureIdentifier().memberLogicalName()); + if(objectMember instanceof OneToOneAssociation prop) + return logicalMemberIdentifierFor(prop.getDeclaringType(), prop.getFeatureIdentifier().memberLogicalName()); + throw new IllegalArgumentException(objectMember.getClass() + " is not supported"); } - + /** * Whether this head corresponds to a mixin. */ public boolean isMixin() { return target.objSpec().isMixin(); } - + // -- HELPER - + private String logicalMemberIdentifierFor(final ObjectSpecification onType, final String memberId) { return onType.logicalTypeName() + "#" + memberId; } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/acc/ObjectTitleContext.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/acc/ObjectTitleContext.java index 089a80f6db3..d201d10a7a0 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/acc/ObjectTitleContext.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/acc/ObjectTitleContext.java @@ -41,6 +41,15 @@ public record ObjectTitleContext( String title) implements AccessContext { + public ObjectTitleContext( + final Identifier identifier, + final ManagedObject targetObjectAdapter, + final InteractionInitiatedBy interactionMethod) { + this(targetObjectAdapter, identifier, + targetObjectAdapter.getTitle(), + interactionMethod); + } + public ObjectTitleContext( final ManagedObject targetAdapter, final Identifier identifier, diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectService.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectService.java index 040007f0cd1..5f47ed1a650 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectService.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectService.java @@ -21,15 +21,14 @@ import java.util.Objects; import java.util.Optional; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.commons.internal.assertions._Assert; import org.apache.causeway.core.metamodel.facets.object.title.TitleRenderRequest; import org.apache.causeway.core.metamodel.objectmanager.memento.ObjectMemento; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** * (package private) specialization corresponding to {@link Specialization#SERVICE} @@ -102,7 +101,7 @@ public final String toString() { private void assertInjectable(final ObjectSpecification spec) { _Assert.assertTrue(spec.isInjectable(), ()->"type %s must be injectable to be considered a service; bean-sort: %s" - .formatted(pojo.getClass(), spec.getBeanSort())); + .formatted(pojo.getClass(), spec.beanSort())); } } \ No newline at end of file diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/Mm2YamlUtils.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/Mm2YamlUtils.java new file mode 100644 index 00000000000..f8f3b136adf --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/Mm2YamlUtils.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.object; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import org.apache.causeway.core.metamodel.spec.feature.MixedIn; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; +import org.jspecify.annotations.Nullable; + +/** + * Introduced for debugging. + */ +public final class Mm2YamlUtils { + + public static String toYaml(final Iterable objSpecs) { + var model = new Model(); + objSpecs.forEach(model::collect); + return model.toYaml(); + } + + public static String toYaml(final Stream objSpecs) { + var model = new Model(); + objSpecs.forEach(model::collect); + return model.toYaml(); + } + + // -- HELPER + + private record Model( + List mixinSpecs, + List valueSpecs, + List serviceSpecs, + List entitySpecs, + List vmSpecs, + List abstractSpecs, + List otherSpecs) { + + private record Writer(StringBuilder sb) { + Writer() { + this(new StringBuilder()); + } + void writeln(final String line) { + sb.append(line).append("\n"); + } + void writeln(final String format, final Object ...args) { + writeln(format.formatted(args)); + } + @Override + public final String toString() { + return sb.toString(); + } + } + + Model() { + this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); + } + + void collect(final ObjectSpecification objSpec) { + switch (objSpec.beanSort()) { + case MIXIN -> mixinSpecs.add(objSpec); + case VALUE -> valueSpecs.add(objSpec); + case MANAGED_BEAN_CONTRIBUTING, MANAGED_BEAN_NOT_CONTRIBUTING -> serviceSpecs.add(objSpec); + case ENTITY -> entitySpecs.add(objSpec); + case VIEW_MODEL -> vmSpecs.add(objSpec); + case ABSTRACT -> abstractSpecs.add(objSpec); + case COLLECTION, PROGRAMMATIC, UNKNOWN, VETOED -> otherSpecs.add(objSpec); + }; + } + + String toYaml() { + var writer = new Writer(); + category(writer, "Mixins", mixinSpecs); + category(writer, "Values", valueSpecs); + category(writer, "Services", serviceSpecs); + category(writer, "Entities", entitySpecs); + category(writer, "Viewmodels", vmSpecs); + category(writer, "Abstract", abstractSpecs); + category(writer, "Other", otherSpecs); + return writer.toString(); + } + + private void category(final Writer writer, final String name, final List specs) { + writer.writeln("%s (count=%d):", name, specs.size()); + specs.stream().sorted() + .forEach(spec->writer.writeln("- {class=%s%s, ract={%s}, rass={%s}}", + spec.getFullIdentifier(), + formatSuper(spec.superclass()), + formatRegularActions(spec), + formatRegularAssociations(spec) + )); + } + + private String formatSuper(@Nullable final ObjectSpecification spec) { + return spec == null + || spec.getCorrespondingClass().equals(java.lang.Record.class) + || spec.getCorrespondingClass().equals(java.lang.Object.class) + ? "" + : ", super=" + spec.getFullIdentifier(); + } + + private String formatRegularActions(final ObjectSpecification spec) { + return spec.streamRuntimeActions(MixedIn.EXCLUDED) + .map(ObjectAction::getId) + .collect(Collectors.joining(", ")); + } + + private String formatRegularAssociations(final ObjectSpecification spec) { + return spec.streamAssociations(MixedIn.EXCLUDED) + .map(ObjectAssociation::getId) + .collect(Collectors.joining(", ")); + } + + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmEntityUtils.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmEntityUtils.java index 2f59e567d83..95abd487c13 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmEntityUtils.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmEntityUtils.java @@ -21,9 +21,6 @@ import java.util.Optional; import java.util.stream.Stream; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - import org.apache.causeway.applib.services.repository.EntityState; import org.apache.causeway.commons.internal.assertions._Assert; import org.apache.causeway.commons.internal.exceptions._Exceptions; @@ -34,6 +31,8 @@ import org.apache.causeway.core.metamodel.services.objectlifecycle.PropertyChangeRecordId; import org.apache.causeway.core.metamodel.spec.feature.MixedIn; import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import lombok.experimental.UtilityClass; @@ -42,13 +41,11 @@ public final class MmEntityUtils { @NonNull public Optional getPersistenceStandard(final @Nullable ManagedObject adapter) { - if(adapter==null) { - return Optional.empty(); - } + if(adapter==null) + return Optional.empty(); var spec = adapter.objSpec(); - if(spec==null || !spec.isEntity()) { - return Optional.empty(); - } + if(spec==null || !spec.isEntity()) + return Optional.empty(); return spec.entityFacet() .map(EntityFacet::getPersistenceStack); @@ -73,7 +70,7 @@ public void deleteInCurrentTransaction(final ManagedObject managedObject) { entityFacet.delete(managedObject.getPojo()); } - public T detachedPojo(ObjectManager objectManager, @Nullable T pojo) { + public T detachedPojo(final ObjectManager objectManager, @Nullable final T pojo) { if(pojo == null) return null; var managedObject = objectManager.adapt(pojo); return isAttachedEntity(managedObject) @@ -82,15 +79,13 @@ public T detachedPojo(ObjectManager objectManager, @Nullable T pojo) { } public void requiresEntity(final ManagedObject managedObject) { - if(ManagedObjects.isNullOrUnspecifiedOrEmpty(managedObject)) { - throw _Exceptions.illegalArgument("requires an entity object but got null, unspecified or empty"); - } + if(ManagedObjects.isNullOrUnspecifiedOrEmpty(managedObject)) + throw _Exceptions.illegalArgument("requires an entity object but got null, unspecified or empty"); var spec = managedObject.objSpec(); - if(!spec.isEntity()) { - throw _Exceptions.illegalArgument("not an entity type %s (sort=%s)", + if(!spec.isEntity()) + throw _Exceptions.illegalArgument("not an entity type %s (sort=%s)", spec.getCorrespondingClass(), - spec.getBeanSort()); - } + spec.beanSort()); } /** @@ -101,9 +96,8 @@ public void requiresEntity(final ManagedObject managedObject) { public void ifHasNoOidThenFlush(final @Nullable ManagedObject entity) { if(ManagedObjects.isNullOrUnspecifiedOrEmpty(entity) || !entity.specialization().isEntity() - || entity.isBookmarkMemoized()) { - return; - } + || entity.isBookmarkMemoized()) + return; if(!getEntityState(entity).hasOid()) { entity.getTransactionService().flushTransaction(); // force reassessment: as a side-effect transitions the transient entity to a bookmarked one @@ -149,20 +143,17 @@ public void requiresWhenFirstIsBookmarkableSecondIsAlso( final ManagedObject first, final ManagedObject second) { - if(!ManagedObjects.isIdentifiable(first) || !ManagedObjects.isSpecified(second)) { - return; - } + if(!ManagedObjects.isIdentifiable(first) || !ManagedObjects.isSpecified(second)) + return; var secondSpec = second.objSpec(); - if(secondSpec.isParented() || !secondSpec.isEntity()) { - return; - } + if(secondSpec.isParented() || !secondSpec.isEntity()) + return; - if(!MmEntityUtils.getEntityState(second).hasOid()) { - throw _Exceptions.illegalArgument( + if(!MmEntityUtils.getEntityState(second).hasOid()) + throw _Exceptions.illegalArgument( "can't set a reference to a transient object [%s] from a persistent one [%s]", second, first.getTitle()); - } } // -- PROPERTY CHANGE PUBLISHING diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmSpecUtils.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmSpecUtils.java index a841aec8c4f..16f6ff79359 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmSpecUtils.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmSpecUtils.java @@ -23,8 +23,6 @@ import java.util.List; import java.util.Optional; -import org.jspecify.annotations.Nullable; - import org.apache.causeway.applib.id.LogicalType; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.functional.Try; @@ -32,10 +30,11 @@ import org.apache.causeway.commons.internal.context._Context; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import lombok.AccessLevel; import lombok.Getter; -import org.jspecify.annotations.NonNull; import lombok.experimental.UtilityClass; @UtilityClass @@ -47,15 +46,13 @@ public final class MmSpecUtils { */ public ManagedObject enforceMostSpecificSpecOn(final @NonNull ManagedObject obj) { if(ManagedObjects.isNullOrUnspecifiedOrEmpty(obj) - || ManagedObjects.isPacked(obj)) { - return obj; - } + || ManagedObjects.isPacked(obj)) + return obj; var pojo = ManagedObjects.peekAtPojoOf(obj); var requiredType = pojo.getClass(); var currentSpec = obj.objSpec(); - if(currentSpec.getCorrespondingClass().equals(requiredType)) { - return obj; - } + if(currentSpec.getCorrespondingClass().equals(requiredType)) + return obj; return ManagedObject.adaptSingular(currentSpec.getSpecificationLoader(), pojo); } @@ -94,7 +91,7 @@ public String specificationsBySortAsYaml(final @NonNull CanspecsBySort.putElement(spec.getBeanSort().name(), spec.logicalType())); + .forEach(spec->specsBySort.putElement(spec.beanSort().name(), spec.logicalType())); // export the list-multi-map to YAML format var sb = new StringBuilder(); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/memento/ObjectDementifierFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/memento/ObjectDementifierFactory.java index 28139e6ba2e..6229221ef29 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/memento/ObjectDementifierFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/memento/ObjectDementifierFactory.java @@ -52,7 +52,7 @@ public ManagedObject handle(final MementoRecreateRequest request) { var spec = request.objectSpecification(); var mmc = spec.getMetaModelContext(); // intercept when managed by Spring - return spec.getBeanSort().policy().isInjectable() + return spec.beanSort().policy().isInjectable() ? mmc.lookupServiceAdapterById(request.memento().logicalType().logicalName()) : mmc.getObjectManager().loadObjectElseFail(request.memento().bookmark()); } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/DescribedAsFromTypePostProcessor.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/DescribedAsFromTypePostProcessor.java index 7db6f25ac6e..e3c6513c27a 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/DescribedAsFromTypePostProcessor.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/DescribedAsFromTypePostProcessor.java @@ -18,8 +18,6 @@ */ package org.apache.causeway.core.metamodel.postprocessors.all; -import jakarta.inject.Inject; - import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.core.metamodel.facetapi.FacetUtil; import org.apache.causeway.core.metamodel.facets.all.described.MemberDescribedFacet; @@ -35,58 +33,58 @@ import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation; import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; +import jakarta.inject.Inject; + public class DescribedAsFromTypePostProcessor extends MetaModelPostProcessorAbstract { @Inject public DescribedAsFromTypePostProcessor(final MetaModelContext mmc) { - super(mmc); + super(mmc, spec->!spec.isMixin()); } @Override - public void postProcessParameter(final ObjectSpecification objectSpecification, final ObjectAction objectAction, final ObjectActionParameter parameter) { + public void postProcessParameter(final ObjectSpecification objSpec, final ObjectAction objectAction, final ObjectActionParameter parameter) { handleParam(parameter); } @Override - public void postProcessAction(final ObjectSpecification objectSpecification, final ObjectAction objectAction) { + public void postProcessAction(final ObjectSpecification objSpec, final ObjectAction objectAction) { handleMember(objectAction); } @Override - public void postProcessProperty(final ObjectSpecification objectSpecification, final OneToOneAssociation prop) { + public void postProcessProperty(final ObjectSpecification objSpec, final OneToOneAssociation prop) { handleMember(prop); } @Override - public void postProcessCollection(final ObjectSpecification objectSpecification, final OneToManyAssociation coll) { + public void postProcessCollection(final ObjectSpecification objSpec, final OneToManyAssociation coll) { handleMember(coll); } // -- HELPER private void handleMember(final ObjectMember member) { - if(member.containsNonFallbackFacet(MemberDescribedFacet.class)) { - return; - } + if(member.containsNonFallbackFacet(MemberDescribedFacet.class)) + return; member.getElementType() - .lookupNonFallbackFacet(ObjectDescribedFacet.class) - .ifPresent(objectDescribedFacet -> - FacetUtil.addFacetIfPresent( + .lookupNonFallbackFacet(ObjectDescribedFacet.class) + .ifPresent(objectDescribedFacet -> + FacetUtil.addFacetIfPresent( MemberDescribedFacetFromType - .create(objectDescribedFacet, facetedMethodFor(member)))); + .create(objectDescribedFacet, facetedMethodFor(member)))); } private void handleParam(final ObjectActionParameter parameter) { - if(parameter.containsNonFallbackFacet(ParamDescribedFacet.class)) { - return; - } + if(parameter.containsNonFallbackFacet(ParamDescribedFacet.class)) + return; parameter.getElementType() - .lookupNonFallbackFacet(ObjectDescribedFacet.class) - .ifPresent(objectDescribedFacet-> - FacetUtil.addFacetIfPresent( + .lookupNonFallbackFacet(ObjectDescribedFacet.class) + .ifPresent(objectDescribedFacet-> + FacetUtil.addFacetIfPresent( ParamDescribedFacetFromType - .create(objectDescribedFacet, parameter.getFacetHolder()))); + .create(objectDescribedFacet, parameter.getFacetHolder()))); } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/MixinSanityChecksValidator.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/MixinSanityChecksValidator.java index 4723a182d6b..35b01a1e3f7 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/MixinSanityChecksValidator.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/MixinSanityChecksValidator.java @@ -112,7 +112,7 @@ private void checkMixinSort(final ObjectSpecification objSpec, final FacetedMeth private void checkMixinMainMethod(final ObjectSpecification objSpec, final Identifier memberIdentifier) { var mixinFacet = objSpec.mixinFacet().orElseThrow(); - var expectedMethodName = mixinFacet.getMainMethodName(); + var expectedMethodName = mixinFacet.mainMethodName(); var actualMethodName = memberIdentifier.memberLogicalName(); if(!expectedMethodName.equals(actualMethodName)) { diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/SanityChecksValidator.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/SanityChecksValidator.java index 854e96c6d04..df17df4cd85 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/SanityChecksValidator.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/SanityChecksValidator.java @@ -18,8 +18,6 @@ */ package org.apache.causeway.core.metamodel.postprocessors.all; -import jakarta.inject.Inject; - import org.apache.causeway.commons.internal.assertions._Assert; import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.core.metamodel.facetapi.FacetHolder; @@ -32,6 +30,8 @@ import org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract; import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailureUtils; +import jakarta.inject.Inject; + /** * Checks various preconditions for a sane meta-model. *
    @@ -96,7 +96,7 @@ private void checkElementType( final ObjectSpecification elementType) { if(elementType == null - || !elementType.getBeanSort().policy().isAllowedAsMemberElementType()) { + || !elementType.beanSort().policy().isAllowedAsMemberElementType()) { ValidationFailureUtils.raiseMemberInvalidElementType(facetHolder, declaringType, elementType); } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/members/navigation/SynthesizeNavigationActionsPostProcessor.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/members/navigation/SynthesizeNavigationActionsPostProcessor.java deleted file mode 100644 index 056a06211f9..00000000000 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/members/navigation/SynthesizeNavigationActionsPostProcessor.java +++ /dev/null @@ -1,56 +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. - */ -package org.apache.causeway.core.metamodel.postprocessors.members.navigation; - -import org.apache.causeway.core.metamodel.context.MetaModelContext; -import org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract; -import org.apache.causeway.core.metamodel.spec.ObjectSpecification; -import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable; - -/** - * Synthesizes the synthetic navigation ("selector") actions for parented collections (and scalar - * references) once per type, during the post-processing phase. - * - *

    - * Active only when command-log recording-support is enabled (see - * {@code causeway.extensions.command-log.recording-support}); a no-op otherwise. The gating is performed - * per-type inside {@link ObjectSpecificationMutable#synthesizeNavigationActions()} (read live from - * configuration), rather than via {@link #isEnabled()}, so that it behaves correctly regardless of when the - * post-processor pipeline is initialized relative to configuration. - * - *

    - * Performing synthesis here (rather than from the lazy {@code streamDeclaredActions} path) keeps it out of - * ordinary action access, so it no longer forces re-entrant introspection of collection element types and - * cannot recurse without bound on a cyclic collection graph. - */ -public class SynthesizeNavigationActionsPostProcessor - extends MetaModelPostProcessorAbstract { - - public SynthesizeNavigationActionsPostProcessor(final MetaModelContext mmc) { - super(mmc); - } - - @Override - public void postProcessObject(final ObjectSpecification objSpec) { - if (objSpec instanceof ObjectSpecificationMutable mutable) { - mutable.synthesizeNavigationActions(); - } - } - -} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/param/ChoicesAndDefaultsPostProcessor.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/param/ChoicesAndDefaultsPostProcessor.java index cd966d5b6af..39685c7cd5c 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/param/ChoicesAndDefaultsPostProcessor.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/param/ChoicesAndDefaultsPostProcessor.java @@ -234,7 +234,7 @@ private static void addCollectionParamChoicesFacetIfNoneAlready( private void checkParamHasChoicesOrAutoCompleteWhenRequired(final ObjectActionParameter param) { var elementType = param.getElementType(); if(elementType == null - || !elementType.getBeanSort().policy().isAllowedAsMemberElementType()) { + || !elementType.beanSort().policy().isAllowedAsMemberElementType()) { // ignore, as these cases are covered later by meta-model validation return; } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefault.java index e80d1be68fd..5ee1342d157 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefault.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/appfeat/ApplicationFeatureRepositoryDefault.java @@ -332,8 +332,8 @@ protected boolean exclude(final ObjectSpecification spec) { var excluded = spec.isMixin() || spec.isAbstract() - || spec.getBeanSort().isVetoed() - || spec.getBeanSort().isUnknown() + || spec.beanSort().isVetoed() + || spec.beanSort().isUnknown() || isBuiltIn(spec) || isHidden(spec); @@ -341,8 +341,8 @@ protected boolean exclude(final ObjectSpecification spec) { log.debug("{} excluded because: abstract:{} vetoed:{} unknown-sort:{} builtIn:{} hidden:{}", spec.getCorrespondingClass().getSimpleName(), spec.isAbstract(), - spec.getBeanSort().isVetoed(), - spec.getBeanSort().isUnknown(), + spec.beanSort().isVetoed(), + spec.beanSort().isUnknown(), isBuiltIn(spec), isHidden(spec) ); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutor.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutor.java index 19df9d0cd0b..305e13048cf 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutor.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutor.java @@ -25,7 +25,7 @@ import org.jspecify.annotations.Nullable; /** - * Provides capability to translate or ignore classes. + * Provides capability to ignore or map classes. */ public interface ClassSubstitutor { diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorAbstract.java index c2cc6c43b52..4e4628b9bb0 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorAbstract.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorAbstract.java @@ -27,8 +27,8 @@ import org.apache.causeway.commons.internal.reflection._ClassCache; import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants; import org.apache.causeway.core.metamodel.commons.ClassUtil; - import org.jspecify.annotations.NonNull; + import lombok.extern.slf4j.Slf4j; @Slf4j @@ -39,45 +39,36 @@ public abstract class ClassSubstitutorAbstract implements ClassSubstitutor { @Override public final Substitution getSubstitution(final @NonNull Class cls) { var replacement = getReplacement(cls); - if(Objects.equals(cls, replacement)) { - return Substitution.passThrough(); // indifferent - } - if(replacement==null) { - return Substitution.neverIntrospect(); - } + if(Objects.equals(cls, replacement)) + return Substitution.passThrough(); // indifferent + if(replacement==null) + return Substitution.neverIntrospect(); return Substitution.replaceWith(replacement) ; } protected Class getReplacement(final Class cls) { - - if(cls == null) { - return null; - } + if(cls == null) + return null; if(proxyPackageNamesToSkip.stream() - .anyMatch(packageName -> cls.getName().startsWith(packageName))) { - return getReplacement(cls.getSuperclass()); - } + .anyMatch(packageName -> cls.getName().startsWith(packageName))) + return getReplacement(cls.getSuperclass()); - if (shouldIgnore(cls)) { - return null; - } + if (shouldIgnore(cls)) + return null; // primarily to ignore unit test fixtures if they happen to be on the classpath. // (we can't simply ignore them; for example ApplicationFeatureType enum // uses anonymous inner classes and these *are* part of the metamodel) - if(cls.isAnonymousClass()) { - return cls.getSuperclass(); - } + if(cls.isAnonymousClass()) + return cls.getSuperclass(); final Class superclass = cls.getSuperclass(); - if(superclass != null && superclass.isEnum()) { - return superclass; - } - if (ClassUtil.directlyImplements(cls, ProxyFactoryService.ProxyEnhanced.class)) { - // REVIEW: arguably this should now go back to the ClassSubstitorRegistry + if(superclass != null && superclass.isEnum()) + return superclass; + if (ClassUtil.directlyImplements(cls, ProxyFactoryService.ProxyEnhanced.class)) + // REVIEW: arguably this should now go back to the ClassSubstitorRegistry return getReplacement(cls.getSuperclass()); - } try { // guard against cannot introspect @@ -123,14 +114,12 @@ protected void skipProxyPackage(final String packageName) { } private boolean shouldIgnore(final Class cls) { - if (cls.isArray()) { - return shouldIgnore(cls.getComponentType()); - } + if (cls.isArray()) + return shouldIgnore(cls.getComponentType()); // ignore vetoed types - if(ProgrammingModelConstants.TypeVetoMarker.anyMatchOn(cls)) { - return true; - } + if(ProgrammingModelConstants.TypeVetoMarker.anyMatchOn(cls)) + return true; var className = cls.getName(); @@ -142,11 +131,10 @@ private boolean shouldIgnore(final Class cls) { } catch(NoClassDefFoundError e) { try{ - if(cls.isAnonymousClass()) { - return shouldIgnore(cls.getSuperclass()); - } else { - return false; - } + if(cls.isAnonymousClass()) + return shouldIgnore(cls.getSuperclass()); + else + return false; } catch(NoClassDefFoundError ex) { return true; } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorDefault.java index 0bccbb6e227..ac13dc9b318 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorDefault.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorDefault.java @@ -18,17 +18,14 @@ */ package org.apache.causeway.core.metamodel.services.classsubstitutor; -import jakarta.inject.Named; - -import org.springframework.stereotype.Component; - -import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.graph.tree.TreeAdapter; import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel; +import org.springframework.stereotype.Component; + +import jakarta.inject.Named; @Component @Named(CausewayModuleCoreMetamodel.NAMESPACE + ".ClassSubstitutorDefault") -@jakarta.annotation.Priority(PriorityPrecedence.MIDPOINT) public class ClassSubstitutorDefault extends ClassSubstitutorAbstract { public ClassSubstitutorDefault() { diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorForCollections.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorForCollections.java index 0836b7d66c4..7c4020d6184 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorForCollections.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorForCollections.java @@ -18,29 +18,22 @@ */ package org.apache.causeway.core.metamodel.services.classsubstitutor; -import jakarta.inject.Named; - -import org.springframework.stereotype.Component; - -import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.commons.semantics.CollectionSemantics; import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel; - import org.jspecify.annotations.NonNull; +import org.springframework.stereotype.Component; + +import jakarta.inject.Named; @Component @Named(CausewayModuleCoreMetamodel.NAMESPACE + ".ClassSubstitutorForCollections") -@jakarta.annotation.Priority(PriorityPrecedence.MIDPOINT - 10) -public class ClassSubstitutorForCollections implements ClassSubstitutor { +public record ClassSubstitutorForCollections() implements ClassSubstitutor { @Override public Substitution getSubstitution(final @NonNull Class cls) { - return CollectionSemantics.valueOf(cls) .map(CollectionSemantics::getContainerType) .map(Substitution::replaceWith) // replace container type with first replacement type that matches - .orElse( Substitution.passThrough()) // indifferent - ; - + .orElse(Substitution.passThrough()); // indifferent } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorForDomainObjects.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorForDomainObjects.java index 3eeb0577285..97a3943e067 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorForDomainObjects.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorForDomainObjects.java @@ -18,31 +18,20 @@ */ package org.apache.causeway.core.metamodel.services.classsubstitutor; -import jakarta.inject.Inject; -import jakarta.inject.Named; - -import org.springframework.stereotype.Component; - -import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.services.metamodel.BeanSort; import org.apache.causeway.applib.services.metamodel.BeanSort.BeanPolicy; import org.apache.causeway.core.config.beans.CausewayBeanMetaData; import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry; import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel; - import org.jspecify.annotations.NonNull; +import org.springframework.stereotype.Component; + +import jakarta.inject.Named; @Component @Named(CausewayModuleCoreMetamodel.NAMESPACE + ".ClassSubstitutorForDomainObjects") -@jakarta.annotation.Priority(PriorityPrecedence.MIDPOINT - 20) // before ClassSubstitutorForCollections -public class ClassSubstitutorForDomainObjects implements ClassSubstitutor { - - private CausewayBeanTypeRegistry causewayBeanTypeRegistry; - - @Inject - public ClassSubstitutorForDomainObjects(final CausewayBeanTypeRegistry causewayBeanTypeRegistry) { - this.causewayBeanTypeRegistry = causewayBeanTypeRegistry; - } +public record ClassSubstitutorForDomainObjects(CausewayBeanTypeRegistry causewayBeanTypeRegistry) +implements ClassSubstitutor { @Override public Substitution getSubstitution(final @NonNull Class cls) { diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java index 05c699850c9..ff5992146c8 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/init/MetamodelInitializer.java @@ -20,13 +20,6 @@ import java.io.File; -import jakarta.inject.Inject; -import jakarta.inject.Provider; - -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Service; - import org.apache.causeway.applib.events.metamodel.MetamodelEvent; import org.apache.causeway.applib.services.eventbus.EventBusService; import org.apache.causeway.applib.util.schema.ChangesDtoUtils; @@ -39,7 +32,12 @@ import org.apache.causeway.core.config.observation.CausewayObservationIntegration.ObservationProvider; import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import jakarta.inject.Inject; +import jakarta.inject.Provider; import lombok.extern.slf4j.Slf4j; @Service @@ -91,6 +89,7 @@ private void initMetamodel(final SpecificationLoader specificationLoader) { taskList.submit(_ConcurrentContext.forkJoin()); taskList.await(); + taskList.rethrow(); { // log any validation failures, experimental code however, not sure how to best propagate failures var validationResult = specificationLoader.getOrAssessValidationResult(); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelExporter.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelExporter.java index 1b9b4cab7e4..ff49c95c7ed 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelExporter.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelExporter.java @@ -19,6 +19,7 @@ package org.apache.causeway.core.metamodel.services.metamodel; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -74,15 +75,15 @@ public MetaModelExporter( MetamodelDto exportMetaModel(final Config config) { // single type(s) MM export support - var tinyDomain = _Lists.newArrayList(); + var tinyDomain = new ArrayList(); var useTinyDomain = _NullSafe.stream(config.getNamespacePrefixes()) - .map(namespace->specificationLookup.specForLogicalTypeName(namespace)) - .peek(specIfAny->specIfAny.ifPresent(tinyDomain::add)) - .allMatch(Optional::isPresent); + .map(namespace->specificationLookup.specForLogicalTypeName(namespace)) + .peek(specIfAny->specIfAny.ifPresent(tinyDomain::add)) + .allMatch(Optional::isPresent); - if(useTinyDomain) { - return exportTinyDomain(tinyDomain, config); - } + if(useTinyDomain + && !tinyDomain.isEmpty()) + return exportTinyDomain(tinyDomain, config); MetamodelDto metamodelDto = new MetamodelDto(); @@ -130,9 +131,8 @@ MetamodelDto exportMetaModel(final Config config) { objectSpecificationByDomainClassId.put(id, objectSpecification); } } - if(buf.size() > 0) { - throw new IllegalStateException(String.join("\n", buf)); - } + if(buf.size() > 0) + throw new IllegalStateException(String.join("\n", buf)); // phase 3: now copy all domain classes into the metamodel for (final ObjectSpecification objectSpecification : _Lists.newArrayList(domainClassByObjectSpec.keySet())) { @@ -194,15 +194,13 @@ private boolean inNamespacePrefixes( final Config config) { var namespacePrefixes = config.getNamespacePrefixes(); - if(config.isNamespacePrefixAny()) { - return true; // export all - } + if(config.isNamespacePrefixAny()) + return true; // export all var logicalTypeName = specification.logicalTypeName(); for (var prefix : namespacePrefixes) { - if(logicalTypeName.startsWith(prefix)) { - return true; - } + if(logicalTypeName.startsWith(prefix)) + return true; } return false; } @@ -230,9 +228,8 @@ private void addFacetsAndMembersTo( } addFacets(specification, domainClass.getFacets(), config); - if(specification.isValueOrIsParented() || isEnum(specification)) { - return; - } + if(specification.isValueOrIsParented() || isEnum(specification)) + return; if (specification.isInjectable()) { if(specification.isDomainService()) { @@ -434,9 +431,8 @@ private void addFacetAttributes( private void addAttribute( final org.apache.causeway.schema.metamodel.v2.Facet facetType, final String key, final String str) { - if(str == null) { - return; - } + if(str == null) + return; FacetAttr attributeDto = new FacetAttr(); attributeDto.setName(key); attributeDto.setValue(str); @@ -460,7 +456,7 @@ private void sortFacets(final List logicalTypeAndAliasesFor(final LogicalType logicalType) specificationLoader().specForLogicalType(logicalType) .ifPresent(objectSpecification -> { logicalTypes.add(logicalType); - objectSpecification.getAliases().stream().forEach(logicalTypes::add); + objectSpecification.aliases().stream().forEach(logicalTypes::add); }); return Can.ofCollection(logicalTypes); } @@ -130,7 +129,7 @@ public DomainModel getDomainModel() { var specifications = specificationLoader().snapshotSpecifications(); - final List rows = _Lists.newArrayList(); + final List rows = new ArrayList<>(); for (final ObjectSpecification spec : specifications) { if (exclude(spec)) { continue; @@ -184,7 +183,7 @@ public BeanSort sortOf( if(objectSpec == null) return BeanSort.UNKNOWN; - if(objectSpec.getBeanSort().isUnknown() + if(objectSpec.beanSort().isUnknown() && !(mode == Mode.RELAXED)) throw new IllegalArgumentException(String.format( "Unable to determine what sort of domain object this is: '%s'. Originating domainType: '%s'", @@ -192,7 +191,7 @@ public BeanSort sortOf( domainType.getName() )); - return objectSpec.getBeanSort(); + return objectSpec.beanSort(); } @@ -263,7 +262,7 @@ public ObjectGraph exportObjectGraph(final @NonNull BiPredicatefilter.test(spec.getBeanSort(), spec.logicalType())) + .filter(spec->filter.test(spec.beanSort(), spec.logicalType())) .collect(Collectors.toList()); return ObjectGraph .create(new _ObjectGraphFactory(objectSpecs)); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/Hierarchical.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/Hierarchical.java index b37787476e2..8dd021a005d 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/Hierarchical.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/Hierarchical.java @@ -18,7 +18,15 @@ */ package org.apache.causeway.core.metamodel.spec; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.commons.internal.base._NullSafe; +import org.apache.causeway.commons.internal.collections._Streams; +import org.apache.causeway.core.metamodel.facetapi.Facet; +import org.apache.causeway.core.metamodel.facetapi.FacetHolder; public interface Hierarchical { @@ -53,4 +61,47 @@ default boolean isTypeHierarchyRoot() { return superclass()==null; } + static Optional lookupFacet(final Class facetType, + final FacetHolder facetHolder, + final Hierarchical hierarchical) { + // lookup facet holder's facet + Stream facets1 = facetHolder.lookupFacet(facetType).stream(); + + // lookup all interfaces + Stream facets2 = _NullSafe.stream(hierarchical.interfaces()) + .filter(_NullSafe::isPresent) // just in case + .flatMap(interfaceSpec->interfaceSpec.lookupFacet(facetType).stream()); + + // search up the inheritance hierarchy + Stream facets3 = _NullSafe.streamNullable(hierarchical.superclass()) + .flatMap(superSpec->superSpec.lookupFacet(facetType).stream()); + + Stream facetsCombined = _Streams.concat(facets1, facets2, facets3); + + // local class, declared inside this method body, so it is not publicly exposed via this interface + // while the test method is called, collects the first occurrence of a fallback facet + class FallbackFacetFilter implements Predicate { + Q fallback; + + @Override + public boolean test(final Q facet) { + if(facet==null) + return false; + if(!facet.precedence().isFallback()) + return true; + if(fallback == null) { + fallback = facet; + } + return false; + } + } + + var filter = new FallbackFacetFilter(); + + return Optional.ofNullable(facetsCombined + .filter(filter) + .findFirst() + .orElse(filter.fallback)); + } + } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecification.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecification.java index ad4ebccce14..3c076ff27d7 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecification.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecification.java @@ -26,9 +26,6 @@ import java.util.Optional; import java.util.stream.Stream; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - import org.apache.causeway.applib.annotation.DomainObject; import org.apache.causeway.applib.annotation.DomainService; import org.apache.causeway.applib.annotation.Introspection.IntrospectionPolicy; @@ -50,7 +47,6 @@ import org.apache.causeway.core.metamodel.facetapi.FacetHolder; import org.apache.causeway.core.metamodel.facetapi.HasFacetHolder; import org.apache.causeway.core.metamodel.facets.all.described.ObjectDescribedFacet; -import org.apache.causeway.core.metamodel.facets.all.help.HelpFacet; import org.apache.causeway.core.metamodel.facets.all.hide.HiddenFacet; import org.apache.causeway.core.metamodel.facets.all.i8n.noun.HasNoun; import org.apache.causeway.core.metamodel.facets.all.i8n.staatic.HasStaticText; @@ -68,6 +64,7 @@ import org.apache.causeway.core.metamodel.facets.object.value.ValueFacet; import org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacet; import org.apache.causeway.core.metamodel.interactions.InteractionContext; +import org.apache.causeway.core.metamodel.interactions.InteractionUtils; import org.apache.causeway.core.metamodel.interactions.acc.ObjectTitleContext; import org.apache.causeway.core.metamodel.interactions.val.ObjectValidityContext; import org.apache.causeway.core.metamodel.object.ManagedObject; @@ -78,6 +75,8 @@ import org.apache.causeway.core.metamodel.spec.feature.ObjectActionContainer; import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer; import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import lombok.experimental.UtilityClass; @@ -104,7 +103,7 @@ public interface ObjectSpecification class Comparators{ public final Comparator BY_BEANSORT_THEN_LOGICALTYPE = - Comparator.comparing(ObjectSpecification::getBeanSort) + Comparator.comparing(ObjectSpecification::beanSort) .thenComparing(ObjectSpecification::logicalType); public final Comparator FULLY_QUALIFIED_CLASS_NAME = @@ -208,7 +207,7 @@ default Optional lookupMixedInAction(final ObjectSpecification mi * Corresponds to {@link DomainService#aliased()} and * {@link DomainObject#aliased()}. */ - Can getAliases(); + Can aliases(); /** * Returns the (singular) name for objects of this specification. @@ -227,14 +226,6 @@ default Optional lookupMixedInAction(final ObjectSpecification mi */ String getDescription(); - /** - * Returns a help string or lookup reference, if any, of the specification. - *

    - * Corresponds to the {@link HelpFacet#value() value} of {@link HelpFacet}; - * is not necessarily immutable. - */ - String getHelp(); - /** * Returns the title to display of target adapter, rendered within the context * of some other adapter (if any). @@ -278,11 +269,11 @@ default Optional lookupMixedInAction(final ObjectSpecification mi /** * @since 2.0 */ - BeanSort getBeanSort(); + BeanSort beanSort(); /** - * Optionally the mixin sort {@link Contributing}, - * based on whether the corresponding class is a mixin type. + * Optionally how the Mixin is {@link Contributing} (as Action, Property or Collection), + * based on whether the corresponding class is a Mixin type. * @since 2.0 */ Optional contributing(); @@ -293,32 +284,46 @@ default Optional lookupMixedInAction(final ObjectSpecification mi * Create an {@link InteractionContext} representing an attempt to read the * object's title. */ - ObjectTitleContext createTitleInteractionContext( - ManagedObject targetObjectAdapter, - InteractionInitiatedBy invocationMethod); + default ObjectTitleContext createTitleInteractionContext( + final ManagedObject targetObjectAdapter, + final InteractionInitiatedBy invocationMethod) { + return new ObjectTitleContext(getFeatureIdentifier(), targetObjectAdapter, invocationMethod); + } // -- VALIDITY // internal API - ObjectValidityContext createValidityInteractionContext( - final ManagedObject targetAdapter, - final InteractionInitiatedBy interactionInitiatedBy); + /** + * Create an {@link InteractionContext} representing an attempt to save the + * object. + */ + default ObjectValidityContext createValidityInteractionContext( + final ManagedObject targetAdapter, final InteractionInitiatedBy interactionInitiatedBy) { + return new ObjectValidityContext(targetAdapter, getFeatureIdentifier(), interactionInitiatedBy); + } /** * Determines whether the specified object is in a valid state (for example, * so can be persisted); represented as a {@link Consent}. */ - Consent isValid( - final ManagedObject targetAdapter, - final InteractionInitiatedBy interactionInitiatedBy); + default Consent isValid( + final ManagedObject targetAdapter, + final InteractionInitiatedBy interactionInitiatedBy) { + return isValidResult(targetAdapter, interactionInitiatedBy).createConsent(); + } /** * Determines whether the specified object is in a valid state (for example, * so can be persisted); represented as a {@link InteractionResult}. */ - InteractionResult isValidResult( + default InteractionResult isValidResult( final ManagedObject targetAdapter, - final InteractionInitiatedBy interactionInitiatedBy); + final InteractionInitiatedBy interactionInitiatedBy) { + var validityContext = + createValidityInteractionContext( + targetAdapter, interactionInitiatedBy); + return InteractionUtils.isValidResult(this, validityContext); + } // -- FACETS @@ -345,7 +350,7 @@ default boolean isSingular() { * @see #isSingular() */ default boolean isPlural() { - return getBeanSort().isCollection(); + return beanSort().isCollection(); } /** @@ -355,7 +360,7 @@ default boolean isPlural() { * In effect, means has got {@link ValueFacet}. */ default boolean isValue() { - return getBeanSort().isValue() + return beanSort().isValue() || valueFacet().isPresent(); } @@ -363,7 +368,7 @@ default boolean isValue() { * Whether objects of this type are composite values. */ default boolean isCompositeValue() { - return getBeanSort().isValue() + return beanSort().isValue() && valueFacet().map(ValueFacet::isCompositeValueType).orElse(false); } @@ -411,7 +416,7 @@ default boolean isValueOrIsParented() { boolean isDomainService(); default boolean isMixin() { - return getBeanSort().isMixin(); + return beanSort().isMixin(); } /** @@ -439,7 +444,7 @@ default boolean isPrimitive() { } default boolean isAbstract() { - return getBeanSort().isAbstract(); + return beanSort().isAbstract(); } /** @@ -458,8 +463,8 @@ default boolean isComparableOrOrdered() { * Includes abstract types that have {@link EntityFacet}. */ default boolean isEntity() { - return getBeanSort().isEntity() - || (getBeanSort().isAbstract() + return beanSort().isEntity() + || (beanSort().isAbstract() && entityFacet().isPresent()); } @@ -467,8 +472,8 @@ default boolean isEntity() { * Includes abstract types that have {@link ViewModelFacet}. */ default boolean isViewModel() { - return getBeanSort().isViewModel() - || (getBeanSort().isAbstract() + return beanSort().isViewModel() + || (beanSort().isAbstract() && viewmodelFacet().isPresent()); } @@ -501,13 +506,13 @@ default boolean isViewModelOrValueOrVoid() { } /** - * @see #getBeanSort() + * @see #beanSort() */ default boolean isEntityOrViewModelOrAbstract() { // optimized, no need to check facets - return getBeanSort().isViewModel() - || getBeanSort().isEntity() - || getBeanSort().isAbstract(); + return beanSort().isViewModel() + || beanSort().isEntity() + || beanSort().isAbstract(); } /** diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecificationRecord.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecificationRecord.java index 1f2dd43277e..9ae5991d5df 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecificationRecord.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecificationRecord.java @@ -18,41 +18,86 @@ */ package org.apache.causeway.core.metamodel.spec; +import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Stream; +import org.apache.causeway.applib.annotation.Introspection.IntrospectionPolicy; +import org.apache.causeway.applib.annotation.ObjectSupport; +import org.apache.causeway.applib.annotation.ObjectSupport.IconResource; +import org.apache.causeway.applib.annotation.ObjectSupport.IconSize; import org.apache.causeway.applib.annotation.Where; -import org.apache.causeway.applib.id.HasLogicalType; +import org.apache.causeway.applib.fa.FontAwesomeLayers; import org.apache.causeway.applib.id.LogicalType; +import org.apache.causeway.applib.services.metamodel.BeanSort; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.collections.ImmutableEnumSet; +import org.apache.causeway.commons.internal.assertions._Assert; +import org.apache.causeway.commons.internal.base._Strings; +import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; +import org.apache.causeway.core.config.beans.CausewayBeanMetaData; +import org.apache.causeway.core.metamodel.facetapi.Facet; import org.apache.causeway.core.metamodel.facetapi.FacetHolder; import org.apache.causeway.core.metamodel.facetapi.FeatureType; -import org.apache.causeway.core.metamodel.facetapi.HasFacetHolder; +import org.apache.causeway.core.metamodel.facets.actcoll.typeof.TypeOfFacet; +import org.apache.causeway.core.metamodel.facets.all.described.ObjectDescribedFacet; +import org.apache.causeway.core.metamodel.facets.all.named.ObjectNamedFacet; +import org.apache.causeway.core.metamodel.facets.members.cssclass.CssClassFacet; +import org.apache.causeway.core.metamodel.facets.members.iconfa.FaFacet; +import org.apache.causeway.core.metamodel.facets.members.iconfa.FaLayersProvider; +import org.apache.causeway.core.metamodel.facets.object.entity.EntityFacet; +import org.apache.causeway.core.metamodel.facets.object.icon.IconFacet; +import org.apache.causeway.core.metamodel.facets.object.mixin.MixinFacet; +import org.apache.causeway.core.metamodel.facets.object.mixin.MixinFacet.Contributing; +import org.apache.causeway.core.metamodel.facets.object.navparent.NavigableParentFacet; +import org.apache.causeway.core.metamodel.facets.object.title.TitleFacet; +import org.apache.causeway.core.metamodel.facets.object.title.TitleRenderRequest; +import org.apache.causeway.core.metamodel.facets.object.value.ValueFacet; +import org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacet; +import org.apache.causeway.core.metamodel.object.ManagedObject; +import org.apache.causeway.core.metamodel.object.ManagedObjects; import org.apache.causeway.core.metamodel.spec.feature.MixedIn; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; import org.apache.causeway.core.metamodel.spec.feature.ObjectActionContainer; import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer; +import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; +import org.apache.causeway.core.metamodel.spi.EntityTitleSubscriber; +import org.springframework.util.StringUtils; -//TODO[causeway-core-metamodel-CAUSEWAY-3834] WIP +//TODO WIP public record ObjectSpecificationRecord( - LogicalType logicalType, + CausewayBeanMetaData typeMeta, FeatureType featureType, FacetHolder facetHolder, Hierarchical hierarchical, ObjectActionContainer actionContainer, - ObjectAssociationContainer associationContainer) + ObjectAssociationContainer associationContainer, + Can titleSubscribers, + IntrospectionPolicy introspectionPolicy, + Can aliases, + Optional> valueFacet, + Optional entityFacet, + Optional viewmodelFacet, + Optional mixinFacet, + Optional objectNamedFacet, + Optional objectDescribedFacet, + Optional typeOfFacet, // explicit element type + Optional titleFacet, + Optional iconFacet, + Optional faFacet, + Optional navigableParentFacet, + Optional cssClassFacet, + boolean isDomainService, + boolean isInjectable, + boolean isParented, + boolean isImmutable, + boolean isHidden, + Map membersByMethod) implements - HasLogicalType, - HasFacetHolder, - Specification, - ObjectActionContainer, - ObjectAssociationContainer, - Hierarchical - //ObjectSpecification -{ + ObjectSpecification { // -- SPECIFICATION @@ -113,4 +158,143 @@ public record ObjectSpecificationRecord( return associationContainer.streamDeclaredAssociations(mixedIn); } + // -- CONTRACT + + @Override + public int hashCode() { + return getCorrespondingClass().hashCode(); + } + @Override + public boolean equals(final Object o) { + return (o instanceof ObjectSpecification other) + ? Objects.equals(this.getCorrespondingClass(), other.getCorrespondingClass()) + : false; + } + @Override + public String toString() { + return "ObjSpec[class=%s, sort=%s, super=%s]" + .formatted(getFullIdentifier(), beanSort().name(), superclass() == null + ? "Object" + : superclass().getFullIdentifier()); + } + + // -- COMPONENTS AND GETTERS + + @Override public BeanSort beanSort() { return typeMeta.beanSort(); } + @Override public IntrospectionPolicy getIntrospectionPolicy() { return introspectionPolicy; } + @Override public Class getCorrespondingClass() { return typeMeta.getCorrespondingClass(); } + @Override public LogicalType logicalType() { return typeMeta.logicalType(); } + @Override public String getFullIdentifier() { return getCorrespondingClass().getName(); } + @Override public String getShortIdentifier() { return logicalType().logicalSimpleName(); } + + @Override + public Optional getMember(final String memberId) { + if(_Strings.isEmpty(memberId)) + return Optional.empty(); + + var objectAction = getAction(memberId); + if(objectAction.isPresent()) + return objectAction; + + var association = getAssociation(memberId); + if(association.isPresent()) + return association; + + return Optional.empty(); + } + @Override + public Optional getMember(final ResolvedMethod method) { + return Optional.ofNullable(membersByMethod.get(method)); + } + @Override + public String getSingularName() { + return objectNamedFacet + .flatMap(ObjectNamedFacet::translated) + // unexpected code reach, however keep for JUnit testing + .orElseGet(()->"(%s has neither title- nor object-named-facet)" + .formatted(getFullIdentifier())); + } + @Override + public String getDescription() { + return objectDescribedFacet + .map(ObjectDescribedFacet::translated) + .orElse(""); + } + @Override + public String getTitle(final TitleRenderRequest titleRenderRequest) { + if (titleFacet.isPresent()) { + var titleString = titleFacet.get().title(titleRenderRequest); + if(StringUtils.hasLength(titleString)) { + notifyTitleSubscribers(titleRenderRequest, titleString); + return titleString; + } + } + return "%s%s" + .formatted(isInjectable + ? "" + : "Untitled ", + getSingularName()); + } + @Override + public Optional getIcon(final ManagedObject domainObject, final IconSize iconSize) { + if(ManagedObjects.isSpecified(domainObject)) { + _Assert.assertEquals(domainObject.objSpec(), this); + } + return iconFacet + .flatMap(facet->facet.icon(domainObject, iconSize)) + .or(()->faLayers(domainObject) + .map(ObjectSupport.FontAwesomeIconResource::new)); + } + @Override + public Object getNavigableParent(final Object object) { + return navigableParentFacet + .map(facet->facet.navigableParent(object)) + .orElse(null); + } + @Override + public String getCssClass(final ManagedObject domainObject) { + return cssClassFacet + .map(facet->facet.cssClass(domainObject)) + .orElse(null); + } + @Override + public Optional explicitElementSpec() { + return typeOfFacet + .map(TypeOfFacet::elementSpec); + } + @Override + public Optional contributing() { + return mixinFacet() + .map(MixinFacet::contributing); + } + + // -- FACET LOOKUP + + @Override + public Optional lookupFacet(final Class facetType) { + return Hierarchical.lookupFacet(facetType, facetHolder, this); + } + + // -- HELPER + + private Optional faLayers(final ManagedObject domainObject){ + return faFacet + .map(FaFacet::getSpecialization) + .map(either->either.fold( + faStaticFacet->(FaLayersProvider)faStaticFacet, + faImperativeFacet->faImperativeFacet.getFaLayersProvider(domainObject))) + .map(FaLayersProvider::getLayers); + } + + private void notifyTitleSubscribers(final TitleRenderRequest titleRenderRequest, final String titleString) { + if(!isEntity() + || titleSubscribers.isEmpty()) + return; + titleRenderRequest + .object() + .getBookmark() + .ifPresent(bookmark -> + titleSubscribers + .forEach(subscriber -> subscriber.entityTitleIs(bookmark, titleString))); + } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectActionContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectActionContainer.java index 46a5138aa6c..717a54ecfce 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectActionContainer.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectActionContainer.java @@ -153,10 +153,10 @@ default Stream streamAnyActions(final MixedIn mixedIn) { * Returns an array of actions of the specified type, including or excluding * contributed actions as required. */ - Stream streamDeclaredActions(ImmutableEnumSet actionTypes, MixedIn mixedIn); + Stream streamDeclaredActions(ImmutableEnumSet actionScopes, MixedIn mixedIn); - default Stream streamDeclaredActions(final ActionScope type, final MixedIn mixedIn) { - return streamDeclaredActions(ImmutableEnumSet.of(type), mixedIn); + default Stream streamDeclaredActions(final ActionScope actionScope, final MixedIn mixedIn) { + return streamDeclaredActions(ImmutableEnumSet.of(actionScope), mixedIn); } default Stream streamDeclaredActions(final MixedIn mixedIn) { diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociationContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociationContainer.java index c8bbfa12e58..822b86d5135 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociationContainer.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociationContainer.java @@ -21,8 +21,6 @@ import java.util.Optional; import java.util.stream.Stream; -import org.jspecify.annotations.Nullable; - import org.apache.causeway.applib.Identifier; import org.apache.causeway.applib.annotation.Where; import org.apache.causeway.applib.services.metamodel.MetaModelService.AssociationsLookup; @@ -30,6 +28,7 @@ import org.apache.causeway.core.metamodel.interactions.managed.ManagedMember; import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.spec.ObjectSpecificationException; +import org.jspecify.annotations.Nullable; public interface ObjectAssociationContainer { diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ActionContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ActionContainer.java new file mode 100644 index 00000000000..ae1efee7a20 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ActionContainer.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.apache.causeway.applib.annotation.Where; +import org.apache.causeway.commons.collections.ImmutableEnumSet; +import org.apache.causeway.commons.internal.base._Strings; +import org.apache.causeway.core.metamodel.spec.ActionScope; +import org.apache.causeway.core.metamodel.spec.feature.MixedIn; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.metamodel.spec.feature.ObjectActionContainer; + +record ActionContainer( + /** + * scopes as available at runtime + */ + ImmutableEnumSet actionScopesAtRuntime, + List productionActions, + List prototypeActions, + ObjectActionContainer superContainer) +implements ObjectActionContainer { + + // useful types that have no mixin support e.g. value types + static ActionContainer EMPTY = new ActionContainer( + ImmutableEnumSet.noneOf(ActionScope.class), + List.of(), List.of(), //Can.empty(), + null); + + ActionContainer( + final List actionsInOrder, + /** + * scopes as available at runtime + */ + final ImmutableEnumSet actionScopes, + final ObjectActionContainer superContainer) { + this(actionScopes, + catalogue(actionsInOrder, ActionScope.PRODUCTION), + catalogue(actionsInOrder, ActionScope.PROTOTYPE), + superContainer); + } + + @Override + public Optional getAction( + final String id, + final ImmutableEnumSet actionScopes, + final MixedIn mixedIn) { + var declaredAction = getDeclaredAction(id, mixedIn); // no inheritance nor type considered + + if(declaredAction.isPresent()) { + // action found but if its not the right type, stop searching + if(!actionScopes.contains(declaredAction.get().getScope())) + return Optional.empty(); + return declaredAction; + } + + return isTypeHierarchyRoot() + ? Optional.empty() // stop searching + : superContainer.getAction(id, actionScopes, mixedIn); + } + + @Override + public Optional getDeclaredAction( + final String id, + final ImmutableEnumSet actionScopes, + final MixedIn mixedIn) { + return _Strings.isEmpty(id) + ? Optional.empty() + : streamDeclaredActions(actionScopes, mixedIn) + .filter(action-> + id.equals(action.getFeatureIdentifier().getMemberNameAndParameterClassNamesIdentityString()) + || id.equals(action.getFeatureIdentifier().memberLogicalName()) + ) + .findFirst(); + } + + @Override + public Stream streamActions( + final ImmutableEnumSet actionTypes, + final MixedIn mixedIn, + final Consumer onActionOverloaded) { + + var actionStream = isTypeHierarchyRoot() + ? streamDeclaredActions(actionTypes, mixedIn) // stop going deeper + : Stream.concat( + streamDeclaredActions(actionTypes, mixedIn), + superContainer.streamActions(actionTypes, mixedIn)); + + var actionSignatures = new HashSet(); + var actionIds = new HashSet(); + + return actionStream + + // as of contributing super-classes same actions might appear more than once (overriding) + .filter(action->{ + if(action.isMixedIn()) + return true; // do not filter mixedIn actions based on signature + var isUnique = actionSignatures + .add(action.getFeatureIdentifier().getMemberNameAndParameterClassNamesIdentityString()); + return isUnique; + }) + + // ensure we don't emit duplicates + .filter(action->{ + var isUnique = actionIds.add(action.getId()); + if(!isUnique) { + onActionOverloaded.accept(action); + } + return isUnique; + }); + } + + @Override + public Stream streamRuntimeActions(final MixedIn mixedIn) { + return streamActions(actionScopesAtRuntime, mixedIn); + } + + @Override + public Stream streamActionsForColumnRendering(final Where where) { + return streamRuntimeActions(MixedIn.INCLUDED) + .filter(ObjectAction.Predicates.visibleAccordingToHiddenFacet(where)) + .sorted((a, b)->a.getCanonicalFriendlyName().compareTo(b.getCanonicalFriendlyName())); + } + + @Override + public Stream streamDeclaredActions( + final ImmutableEnumSet actionScopes, + final MixedIn mixedIn) { + return actionScopes.stream() + .flatMap(actionScope->list(actionScope).stream()) + .filter(mixedIn.toFilter()); + } + + // -- HELPER + + private boolean isTypeHierarchyRoot() { + return superContainer==null; + } + + private List list(final ActionScope actionScope) { + return switch (actionScope) { + case PRODUCTION -> productionActions; + case PROTOTYPE -> prototypeActions; + }; + } + + private static List catalogue( + final List actionsInOrder, + final ActionScope actionScope) { + return actionsInOrder.stream() + .filter(ObjectAction.Predicates.ofActionType(actionScope)) + .toList(); + } +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/AssociationContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/AssociationContainer.java new file mode 100644 index 00000000000..780489a2674 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/AssociationContainer.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.commons.internal.base._Strings; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import org.apache.causeway.core.metamodel.spec.feature.MixedIn; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer; +import org.jspecify.annotations.Nullable; + +record AssociationContainer( + Can associationsInOrder, + ObjectAssociationContainer superContainer, + /** used for column rendering, null in the EMPTY case */ + @Nullable ObjectSpecification correspondingSpec) +implements ObjectAssociationContainer { + + // e.g. used for value types + static AssociationContainer EMPTY = new AssociationContainer( + Can.empty(), + null, + null); + + AssociationContainer( + final List associationsInOrder, + final ObjectAssociationContainer superContainer, + final ObjectSpecification correspondingSpec) { + this(Can.ofCollection(associationsInOrder), superContainer, correspondingSpec); + } + + @Override + public Optional getAssociation(final String id, final MixedIn mixedIn) { + + var declaredAssociation = getDeclaredAssociation(id, mixedIn); // no inheritance considered + + if(declaredAssociation.isPresent()) + return declaredAssociation; + + return isTypeHierarchyRoot() + ? Optional.empty() // stop searching + : superContainer.getAssociation(id, mixedIn); + } + + @Override + public Stream streamAssociations(final MixedIn mixedIn) { + if(isTypeHierarchyRoot()) + return streamDeclaredAssociations(mixedIn); // stop going deeper + + var ids = new HashSet(); + + return Stream.concat( + streamDeclaredAssociations(mixedIn), + superContainer.streamAssociations(mixedIn) + ) + .filter(association->ids.add(association.getId())); // ensure we don't emit duplicates + } + + @Override + public Optional getDeclaredAssociation(final String id, final MixedIn mixedIn) { + if(_Strings.isEmpty(id)) + return Optional.empty(); + + return streamDeclaredAssociations(mixedIn) + .filter(objectAssociation->objectAssociation.getId().equals(id)) + .findFirst(); + } + + @Override + public Stream streamAssociationsForColumnRendering(final ColumnQuery columnQuery) { + if(correspondingSpec==null) + return Stream.empty(); + return new _MembersAsColumns(correspondingSpec.getMetaModelContext()) + .streamAssociationsForColumnRendering(correspondingSpec, columnQuery); + } + + @Override + public Stream streamDeclaredAssociations(final MixedIn mixedIn) { + return associationsInOrder.stream() + .filter(mixedIn.toFilter()); + } + + // -- HELPER + + private boolean isTypeHierarchyRoot() { + return superContainer==null; + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetProcessor.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetProcessor.java index 88a35bf628b..c930d3c2b38 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetProcessor.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetProcessor.java @@ -28,6 +28,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.NonNull; + import org.apache.causeway.applib.annotation.Introspection.IntrospectionPolicy; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.internal.collections._Multimaps; @@ -52,9 +54,7 @@ import org.apache.causeway.core.metamodel.methods.MethodFilteringFacetFactory; import org.apache.causeway.core.metamodel.methods.MethodPrefixBasedFacetFactory; import org.apache.causeway.core.metamodel.progmodel.ProgrammingModel; -import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; -import org.jspecify.annotations.NonNull; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -290,7 +290,7 @@ public void process( featureType, method, removerElseNoopRemover(methodRemover), facetedMethod, isMixinMain); - + // warn on parameter names NOT reflectable if(processMethodContext.hasPotentialNonReflectableParameterNames()) { log.warn("potential missing -parameters compiler flag with method {}#{}", @@ -303,10 +303,6 @@ public void process( } } - public void processMemberOrder(final ObjectMember facetHolder) { - - } - /** * Attaches all facets applicable to the provided parameter to the supplied * {@link FacetHolder}. @@ -375,7 +371,9 @@ private static List propertyAccessorFactories(final Iterab var propertyOrCollectionIdentifyingFactories = new ArrayList(); for (var factory : factories) { if (factory instanceof AccessorFacetFactory accessorFacetFactory) { - if(!accessorFacetFactory.supportsProperties()) continue; + if(!accessorFacetFactory.supportsProperties()) { + continue; + } propertyOrCollectionIdentifyingFactories.add(accessorFacetFactory); } } @@ -385,7 +383,9 @@ private static List collectionAccessorFactories(final Iter var propertyOrCollectionIdentifyingFactories = new ArrayList(); for (var factory : factories) { if (factory instanceof AccessorFacetFactory accessorFacetFactory) { - if(!accessorFacetFactory.supportsCollections()) continue; + if(!accessorFacetFactory.supportsCollections()) { + continue; + } propertyOrCollectionIdentifyingFactories.add(accessorFacetFactory); } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsBuilder.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsFactory.java similarity index 65% rename from core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsBuilder.java rename to core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsFactory.java index 3b584473d03..975466c0718 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsBuilder.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsFactory.java @@ -19,33 +19,28 @@ package org.apache.causeway.core.metamodel.spec.impl; import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.jspecify.annotations.Nullable; - import org.apache.causeway.applib.annotation.Action; import org.apache.causeway.applib.annotation.Introspection.IntrospectionPolicy; import org.apache.causeway.applib.exceptions.unrecoverable.MetaModelException; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.internal.base._NullSafe; -import org.apache.causeway.commons.internal.collections._Lists; -import org.apache.causeway.commons.internal.collections._Sets; import org.apache.causeway.commons.internal.reflection._Annotations; import org.apache.causeway.commons.internal.reflection._ClassCache; import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; import org.apache.causeway.commons.internal.reflection._MethodFacades; import org.apache.causeway.commons.internal.reflection._MethodFacades.MethodFacade; import org.apache.causeway.commons.internal.reflection._Reflect; -import org.apache.causeway.core.metamodel.commons.ToString; import org.apache.causeway.core.metamodel.context.HasMetaModelContext; import org.apache.causeway.core.metamodel.facetapi.FeatureType; import org.apache.causeway.core.metamodel.facetapi.MethodRemover; @@ -54,30 +49,35 @@ import org.apache.causeway.core.metamodel.facets.actcoll.typeof.TypeOfFacet; import org.apache.causeway.core.metamodel.facets.object.mixin.MixinFacet; import org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutorRegistry; -import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionRequest; +import org.apache.causeway.core.metamodel.spec.impl.IntrospectionStateHandler.IntrospectionRequest; import org.apache.causeway.core.metamodel.specloader.typeextract.TypeExtractor; +import org.jspecify.annotations.Nullable; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; +// has side-effects: calls specloader @Slf4j -class FacetedMethodsBuilder +record FacetedMethodsFactory( + ObjectSpecificationBuilder specBuilder, + ConcurrentMethodRemover methodRemover, + FacetProcessor facetProcessor, + ClassSubstitutorRegistry classSubstitutorRegistry) implements HasSpecificationLoaderInternal, HasMetaModelContext { - /* thread-safety ... make sure every methodsRemaining access is synchronized! */ - private static final class ConcurrentMethodRemover implements MethodRemover { + private record ConcurrentMethodRemover( + /* thread-safe */ + Set methodsRemaining) implements MethodRemover { - private final Set methodsRemaining; - - private ConcurrentMethodRemover(final Class introspectedClass, final Stream methodStream) { - this.methodsRemaining = methodStream - .collect(Collectors.toCollection(_Sets::newConcurrentHashSet)); - } + static ConcurrentMethodRemover forSpecBuilder(final ObjectSpecificationBuilder specBuilder) { + return new ConcurrentMethodRemover((specBuilder.getIntrospectionPolicy().getEncapsulationPolicy().isEncapsulatedMembersSupported() + ? _ClassCache.getInstance().streamResolvedMethods(specBuilder.getCorrespondingClass()) + : _ClassCache.getInstance().streamPublicMethods(specBuilder.getCorrespondingClass())) + .collect(Collectors.toCollection(ConcurrentHashMap::newKeySet))); + } - @Override - public void removeMethods(final Predicate removeIf, final Consumer onRemoval) { + @Override public void removeMethods(final Predicate removeIf, final Consumer onRemoval) { methodsRemaining.removeIf(method -> { var doRemove = removeIf.test(method); if(doRemove) { @@ -86,129 +86,107 @@ public void removeMethods(final Predicate removeIf, final Consum return doRemove; }); } - - @Override - public void removeMethod(final ResolvedMethod method) { + @Override public void removeMethod(final ResolvedMethod method) { if(method==null) return; methodsRemaining.remove(method); } - - Stream streamRemaining() { - return methodsRemaining.stream(); - } - - @Override - public Can snapshotMethodsRemaining() { + @Override public Can snapshotMethodsRemaining() { return Can.ofCollection(methodsRemaining); } - + private Stream streamRemaining() { + return methodsRemaining.stream(); + } } - private final ObjectSpecificationDefault inspectedTypeSpec; - - @Getter private final Class introspectedClass; - - private List associationFacetMethods; - private List actionFacetedMethods; - - private final ConcurrentMethodRemover methodRemover; - - @Getter private final FacetProcessor facetProcessor; - - private final ClassSubstitutorRegistry classSubstitutorRegistry; - - // -- CONSTRUCTOR - - public FacetedMethodsBuilder( - final ObjectSpecificationDefault inspectedTypeSpec, + FacetedMethodsFactory( + final ObjectSpecificationBuilder specBuilder, final FacetProcessor facetProcessor, final ClassSubstitutorRegistry classSubstitutorRegistry) { + this(specBuilder, ConcurrentMethodRemover.forSpecBuilder(specBuilder), facetProcessor, classSubstitutorRegistry); + } - if (log.isDebugEnabled()) { - log.debug("creating JavaIntrospector for {}", inspectedTypeSpec.getFullIdentifier()); + FacetedMethodsFactory { + if (log.isDebugEnabled()) { + log.debug("creating {} for {}", this.getClass().getSimpleName(), specBuilder.getFullIdentifier()); } - - this.facetProcessor = facetProcessor; - this.classSubstitutorRegistry = classSubstitutorRegistry; - this.inspectedTypeSpec = inspectedTypeSpec; - this.introspectedClass = inspectedTypeSpec.getCorrespondingClass(); - - var classCache = _ClassCache.getInstance(); - var methodsRemaining = introspectionPolicy().getEncapsulationPolicy().isEncapsulatedMembersSupported() - ? classCache.streamResolvedMethods(introspectedClass) - : classCache.streamPublicMethods(introspectedClass); - this.methodRemover = new ConcurrentMethodRemover(introspectedClass, methodsRemaining); } - // //////////////////////////////////////////////////////////////////////////// - // Class and stuff immediately derived from class - // //////////////////////////////////////////////////////////////////////////// - - private String getClassName() { - return introspectedClass.getName(); + Class introspectedClass() { + return specBuilder.getCorrespondingClass(); } - // //////////////////////////////////////////////////////////////////////////// - // introspect class - // //////////////////////////////////////////////////////////////////////////// public void introspectClass() { if (log.isDebugEnabled()) { - log.debug("introspecting {}: class-level details", getClassName()); + log.debug("introspecting {}: class-level details", introspectedClass().getName()); } // process facets at object level // this will also remove some methods, such as the superclass methods. - getFacetProcessor() - .process(introspectedClass, introspectionPolicy(), methodRemover, inspectedTypeSpec); + facetProcessor + .process(introspectedClass(), introspectionPolicy(), methodRemover, specBuilder); } - // //////////////////////////////////////////////////////////////////////////// - // introspect associations - // //////////////////////////////////////////////////////////////////////////// - /** * Returns a {@link List} of {@link FacetedMethod}s representing object * actions, lazily creating them first if required. */ - public List getAssociationFacetedMethods() { - if (associationFacetMethods == null) { - associationFacetMethods = createAssociationFacetedMethods(); + public Stream createActionFacetedMethods() { + if (log.isDebugEnabled()) { + log.debug("introspecting(policy={}) {}: actions", introspectionPolicy(), introspectedClass().getName()); } - return associationFacetMethods; + var actionFacetedMethods = new ArrayList(); + collectActionFacetedMethods(actionFacetedMethods::add); + return actionFacetedMethods.stream(); } - private List createAssociationFacetedMethods() { + /** + * Returns a {@link Stream} of {@link FacetedMethod}s representing object + * actions, lazily creating them first if required. + */ + public Stream createAssociationFacetedMethods() { if (log.isDebugEnabled()) { - log.debug("introspecting(policy={}) {}: properties and collections", introspectionPolicy(), getClassName()); + log.debug("introspecting(policy={}) {}: properties and collections", introspectionPolicy(), introspectedClass().getName()); } - var specLoader = (SpecificationLoaderInternal)getSpecificationLoader(); - var associationCandidateMethods = new HashSet(); - getFacetProcessor() + facetProcessor .findAssociationCandidateGetters( methodRemover.streamRemaining(), associationCandidateMethods::add); // Ensure all return types are known TypeExtractor.streamMethodReturn(associationCandidateMethods) - .filter(typeToLoad->typeToLoad!=introspectedClass) - .forEach(typeToLoad->specLoader.loadSpecification(typeToLoad, IntrospectionRequest.TYPE_ONLY)); + .filter(typeToLoad->typeToLoad!=introspectedClass()) + .forEach(typeToLoad->specBuilder.specLoaderInternal().loadSpecification(typeToLoad, IntrospectionRequest.TYPE_ONLY)); // now create FacetedMethods for collections and for properties var associationFacetedMethods = new ArrayList(); - var collectionAccessors = getFacetProcessor().findAndRemoveCollectionAccessors(methodRemover); + var collectionAccessors = facetProcessor.findAndRemoveCollectionAccessors(methodRemover); createCollectionFacetedMethodsFromAccessors(collectionAccessors, associationFacetedMethods::add); - var propertyAccessors = getFacetProcessor().findAndRemovePropertyAccessors(methodRemover); + var propertyAccessors = facetProcessor.findAndRemovePropertyAccessors(methodRemover); createPropertyFacetedMethodsFromAccessors(propertyAccessors, associationFacetedMethods::add); - return Collections.unmodifiableList(associationFacetedMethods); + return associationFacetedMethods.stream(); } + /** + * exposed for debugging purposes + */ + public Can snapshotMethodsRemaining() { + return methodRemover.snapshotMethodsRemaining(); + } + + @Override + public String toString() { + return "%s[class=%s]".formatted(this.getClass().getSimpleName(), introspectedClass().getName()); + } + + // -- HELPER + private void createCollectionFacetedMethodsFromAccessors( final List accessorMethods, final Consumer onNewFacetMethod) { @@ -223,10 +201,10 @@ private void createCollectionFacetedMethodsFromAccessors( var accessorMethodFacade = _MethodFacades.regular(accessorMethod); // create property and add facets - var facetedMethod = FacetedMethod.createForCollection(mmc, introspectedClass, accessorMethod); - getFacetProcessor() + var facetedMethod = FacetedMethod.createForCollection(mmc, introspectedClass(), accessorMethod); + facetProcessor .process( - introspectedClass, + introspectedClass(), introspectionPolicy(), accessorMethodFacade, methodRemover, @@ -264,14 +242,14 @@ private void createPropertyFacetedMethodsFromAccessors( // create a 1:1 association peer var facetedMethod = FacetedMethod - .createForProperty(getMetaModelContext(), introspectedClass, accessorMethod); + .createForProperty(getMetaModelContext(), introspectedClass(), accessorMethod); var accessorMethodFacade = _MethodFacades.regular(accessorMethod); // process facets for the 1:1 association (eg. contributed properties) - getFacetProcessor() - .process( - introspectedClass, + facetProcessor + .process( + introspectedClass(), introspectionPolicy(), accessorMethodFacade, methodRemover, @@ -283,30 +261,6 @@ private void createPropertyFacetedMethodsFromAccessors( } } - // //////////////////////////////////////////////////////////////////////////// - // introspect actions - // //////////////////////////////////////////////////////////////////////////// - - /** - * Returns a {@link List} of {@link FacetedMethod}s representing object - * actions, lazily creating them first if required. - */ - public List getActionFacetedMethods() { - if (actionFacetedMethods == null) { - actionFacetedMethods = findActionFacetedMethods(); - } - return actionFacetedMethods; - } - - private List findActionFacetedMethods() { - if (log.isDebugEnabled()) { - log.debug("introspecting(policy={}) {}: actions", introspectionPolicy(), getClassName()); - } - var actionFacetedMethods = _Lists.newArrayList(); - collectActionFacetedMethods(actionFacetedMethods::add); - return actionFacetedMethods; - } - private void collectActionFacetedMethods(final Consumer onActionFacetedMethod) { if (log.isDebugEnabled()) { @@ -339,16 +293,16 @@ private FacetedMethod findActionFacetedMethod(final ResolvedMethod actionMethod) @Nullable private FacetedMethod createActionFacetedMethod(final ResolvedMethod actionMethod) { - var actionMethodFacade = _MethodFacadeAutodetect.autodetect(actionMethod, inspectedTypeSpec); + var actionMethodFacade = _MethodFacadeAutodetect.autodetect(actionMethod, specBuilder); if (!isAllParamTypesValid(actionMethodFacade)) return null; final FacetedMethod action = FacetedMethod - .createForAction(getMetaModelContext(), introspectedClass, actionMethodFacade); + .createForAction(getMetaModelContext(), introspectedClass(), actionMethodFacade); // process facets on the action & parameters - getFacetProcessor() - .process( - introspectedClass, + facetProcessor + .process( + introspectedClass(), introspectionPolicy(), actionMethodFacade, methodRemover, @@ -357,10 +311,9 @@ private FacetedMethod createActionFacetedMethod(final ResolvedMethod actionMetho isMixinMain(actionMethodFacade)); action.parameters() - .forEach(actionParam->{ - getFacetProcessor() - .processParams(introspectedClass, introspectionPolicy(), actionMethodFacade, methodRemover, actionParam); - }); + .forEach(actionParam-> + facetProcessor + .processParams(introspectedClass(), introspectionPolicy(), actionMethodFacade, methodRemover, actionParam)); return action; } @@ -400,9 +353,9 @@ private boolean representsAction(final ResolvedMethod actionMethod) { } // exclude those that have eg. reserved prefixes - if (getFacetProcessor().recognizes(actionMethod)) { + if (facetProcessor.recognizes(actionMethod)) { // this is a potential orphan candidate, collect these, than use when validating - inspectedTypeSpec.getPotentialOrphans().add(actionMethod); + specBuilder.getPotentialOrphans().add(actionMethod); return false; } @@ -430,16 +383,16 @@ private boolean isMixinMain(final MethodFacade methodFacade) { * @param method */ private boolean isMixinMain(final ResolvedMethod method) { - var mixinFacet = inspectedTypeSpec.lookupNonFallbackFacet(MixinFacet.class) + var mixinFacet = specBuilder.lookupNonFallbackFacet(MixinFacet.class) .orElse(null); if(mixinFacet==null) return false; - if(!inspectedTypeSpec.isFullyIntrospected()) - // members are not introspected yet, so make a guess - return mixinFacet.isCandidateForMain(method); + if(!specBuilder.isFullyIntrospected()) + // members are not introspected yet, so make a guess + return mixinFacet.isCandidateForMain(method); - return inspectedTypeSpec - .lookupMixedInAction(inspectedTypeSpec) + return specBuilder + .lookupMixedInAction(specBuilder) .map(HasFacetedMethod.class::cast) .map(HasFacetedMethod::getFacetedMethod) .map(FacetedMethod::methodFacade) @@ -449,21 +402,7 @@ private boolean isMixinMain(final ResolvedMethod method) { } private IntrospectionPolicy introspectionPolicy() { - return inspectedTypeSpec.getIntrospectionPolicy(); - } - - @Override - public String toString() { - final ToString str = new ToString(this); - str.append("class", getClassName()); - return str.toString(); - } - - /** - * exposed for debugging purposes - */ - public Can snapshotMethodsRemaining() { - return methodRemover.snapshotMethodsRemaining(); + return specBuilder.getIntrospectionPolicy(); } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasIntrospectionStateHandler.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasIntrospectionStateHandler.java new file mode 100644 index 00000000000..264d4249680 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasIntrospectionStateHandler.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +@FunctionalInterface +interface HasIntrospectionStateHandler extends IntrospectionStateHandler { + + IntrospectionStateHandler introspectionStateHandler(); + + @Override + default void introspectUpTo(final IntrospectionState upTo) { + introspectionStateHandler().introspectUpTo(upTo); + } + + @Override + default boolean isFullyIntrospected() { + return introspectionStateHandler().isFullyIntrospected(); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasObjectActionContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasObjectActionContainer.java new file mode 100644 index 00000000000..a6db8ae6368 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasObjectActionContainer.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.apache.causeway.applib.annotation.Where; +import org.apache.causeway.commons.collections.ImmutableEnumSet; +import org.apache.causeway.core.metamodel.spec.ActionScope; +import org.apache.causeway.core.metamodel.spec.feature.MixedIn; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.metamodel.spec.feature.ObjectActionContainer; + +@FunctionalInterface +interface HasObjectActionContainer extends ObjectActionContainer { + + ObjectActionContainer objectActionContainer(); + + @Override + default Optional getAction( + final String id, + final ImmutableEnumSet actionScopes, + final MixedIn mixedIn) { + return objectActionContainer().getAction(id, actionScopes, mixedIn); + } + + @Override + default Optional getDeclaredAction( + final String id, + final ImmutableEnumSet actionScopes, + final MixedIn mixedIn) { + return objectActionContainer().getDeclaredAction(id, actionScopes, mixedIn); + } + + @Override + default Stream streamActions( + final ImmutableEnumSet actionTypes, + final MixedIn mixedIn, + final Consumer onActionOverloaded) { + return objectActionContainer().streamActions(actionTypes, mixedIn, onActionOverloaded); + } + + @Override + default Stream streamRuntimeActions(final MixedIn mixedIn) { + return objectActionContainer().streamRuntimeActions(mixedIn); + } + + @Override + default Stream streamActionsForColumnRendering(final Where where) { + return objectActionContainer().streamActionsForColumnRendering(where); + } + + @Override + default Stream streamDeclaredActions( + final ImmutableEnumSet actionScopes, + final MixedIn mixedIn) { + return objectActionContainer().streamDeclaredActions(actionScopes, mixedIn); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasObjectAssociationContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasObjectAssociationContainer.java new file mode 100644 index 00000000000..eb0b8a129ad --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasObjectAssociationContainer.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +import java.util.Optional; +import java.util.stream.Stream; + +import org.apache.causeway.core.metamodel.spec.feature.MixedIn; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer; + +@FunctionalInterface +interface HasObjectAssociationContainer extends ObjectAssociationContainer { + + ObjectAssociationContainer objectAssociationContainer(); + + @Override + default Optional getAssociation(final String id, final MixedIn mixedIn) { + return objectAssociationContainer().getAssociation(id, mixedIn); + } + + @Override + default Stream streamAssociations(final MixedIn mixedIn) { + return objectAssociationContainer().streamAssociations(mixedIn); + } + + @Override + default Optional getDeclaredAssociation(final String id, final MixedIn mixedIn) { + return objectAssociationContainer().getDeclaredAssociation(id, mixedIn); + } + + @Override + default Stream streamDeclaredAssociations(final MixedIn mixedIn) { + return objectAssociationContainer().streamDeclaredAssociations(mixedIn); + } + + @Override + default Stream streamAssociationsForColumnRendering(final ColumnQuery columnQuery) { + return objectAssociationContainer().streamAssociationsForColumnRendering(columnQuery); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionStateHandler.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionStateHandler.java new file mode 100644 index 00000000000..1f669d37ea9 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionStateHandler.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +import org.apache.causeway.applib.id.LogicalType; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; + +interface IntrospectionStateHandler { + + enum IntrospectionState { + /** + * At this stage, {@link LogicalType} only. + */ + NOT_INTROSPECTED, + /** + * Interim stage, to avoid infinite loops while on way to being {@link #TYPE_INTROSPECTED} + */ + TYPE_BEING_INTROSPECTED, + /** + * Type has been introspected (but not its members). + */ + TYPE_INTROSPECTED, + /** + * Interim stage, to avoid infinite loops while on way to being {@link #FULLY_INTROSPECTED} + */ + MEMBERS_BEING_INTROSPECTED, + + //MIXED_IN_MEMBERS_ADDED, + /** + * Fully introspected... class and also its members. + */ + FULLY_INTROSPECTED; + + boolean isLessThan(final IntrospectionState other) { + return this.ordinal() < other.ordinal(); + } + } + + enum IntrospectionRequest { + /** + * No introspection, just register the type, that is, create an initial yet empty {@link ObjectSpecification}. + */ + REGISTER, + /** + * Partial introspection, that only includes type-hierarchy but not members. + */ + TYPE_ONLY, + /** + * Full introspection, that includes type-hierarchy and members. + */ + FULL + } + + void introspectUpTo(final IntrospectionState upTo); + boolean isFullyIntrospected(); + + default void introspect(final IntrospectionRequest request) { + switch (request) { + case REGISTER -> register(); + case TYPE_ONLY -> introspectTypeOnly(); + case FULL -> introspectFully(); + } + } + + /** + * No introspection, just register the type, that is, create an initial yet empty {@link ObjectSpecification}. + */ + default void register() { + introspectUpTo(IntrospectionState.NOT_INTROSPECTED); + } + + /** + * Partial introspection, that only includes type-hierarchy but not members. + */ + default void introspectTypeOnly() { + introspectUpTo(IntrospectionState.TYPE_INTROSPECTED); + } + + /** + * Full introspection, that includes type-hierarchy and members. + */ + default void introspectFully() { + introspectUpTo(IntrospectionState.FULLY_INTROSPECTED); + } + +} \ No newline at end of file diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionStateHandlerThreadSafe.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionStateHandlerThreadSafe.java new file mode 100644 index 00000000000..ed89a8870da --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionStateHandlerThreadSafe.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +/** + * Guarantees thread-safe state transition. + */ +final class IntrospectionStateHandlerThreadSafe +implements IntrospectionStateHandler { + + private final Runnable introspectTypeHierarchy; + private final Runnable introspectMembers; + private final Object lock = new Object(); + private IntrospectionState state; + + IntrospectionStateHandlerThreadSafe( + final Runnable introspectTypeHierarchy, + final Runnable introspectMembers) { + this.introspectTypeHierarchy = introspectTypeHierarchy; + this.introspectMembers = introspectMembers; + this.state = IntrospectionState.NOT_INTROSPECTED; + } + + @Override + public boolean isFullyIntrospected() { + // we don't care about thread synchronization here + return state == IntrospectionState.FULLY_INTROSPECTED; + } + + @Override + public void introspectUpTo(final IntrospectionState upTo) { + if(isFullyIntrospected()) + return; // optimization + + switch (state) { + case NOT_INTROSPECTED->{ + if(state.isLessThan(upTo)) { + transitionToTypeIntrospected(); + } + if(state.isLessThan(upTo)) { + transitionToFullyIntrospected(); + } + } + case TYPE_BEING_INTROSPECTED->{} // nothing to do (interim state during introspectType) + case TYPE_INTROSPECTED->{ + if(state.isLessThan(upTo)) { + transitionToFullyIntrospected(); + } + } + case MEMBERS_BEING_INTROSPECTED->{}// nothing to do (interim state during introspect fully) + case FULLY_INTROSPECTED->{}// nothing to do ... all done + } + } + + // -- HELPER + + private void transitionToTypeIntrospected() { + // This ensures only one thread changes state at a time, + // but threads block while another thread holds the lock. + synchronized (lock) { + if(!state.isLessThan(IntrospectionState.TYPE_BEING_INTROSPECTED)) + return; // in case the state had changed till acquiring the lock + this.state = IntrospectionState.TYPE_BEING_INTROSPECTED; + introspectTypeHierarchy.run(); + this.state = IntrospectionState.TYPE_INTROSPECTED; + } + } + + private void transitionToFullyIntrospected() { + // This ensures only one thread changes state at a time, + // but threads block while another thread holds the lock. + synchronized (lock) { + if(!state.isLessThan(IntrospectionState.MEMBERS_BEING_INTROSPECTED)) + return; // in case the state had changed till acquiring the lock + this.state = IntrospectionState.MEMBERS_BEING_INTROSPECTED; + introspectMembers.run(); + this.state = IntrospectionState.FULLY_INTROSPECTED; + } + } + +} \ No newline at end of file diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/LogicalTypeResolver.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/LogicalTypeResolver.java index b687951eae0..fe234000bee 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/LogicalTypeResolver.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/LogicalTypeResolver.java @@ -93,7 +93,7 @@ && hasTypeIdentity(spec)) { ObjectSpecification registerAliases(final @NonNull ObjectSpecification spec) { // adding aliases to the lookup map - spec.getAliases() + spec.aliases() .forEach(alias->{ putWithWarnOnOverride(alias.logicalName(), spec); }); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MemberCatalog.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MemberCatalog.java new file mode 100644 index 00000000000..2341338f717 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MemberCatalog.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; + +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; +import org.apache.causeway.commons.internal.reflection._MethodFacades.MethodFacade; +import org.apache.causeway.commons.internal.reflection._Reflect; +import org.apache.causeway.core.metamodel.facets.ImperativeFacet; +import org.apache.causeway.core.metamodel.spec.feature.MixedIn; +import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; + +record MemberCatalog( + Map membersByMethod) { + + static MemberCatalog EMPTY = new MemberCatalog(Map.of()); + + MemberCatalog(final ObjectSpecificationBuilder spec) { + this(catalogMembersByMethod(Objects.requireNonNull(spec))); + } + + Optional lookupMember(final ResolvedMethod method) { + if(this==EMPTY) // the EMPTY case + throw new UnsupportedOperationException("members are only available after introspection, lookupMember was probably called too early"); + return Optional.ofNullable(membersByMethod.get(method)); + } + + // -- HELPER + + private static Map catalogMembersByMethod(final ObjectSpecificationBuilder spec) { + var membersByMethod = new HashMap(); + cataloguePropertiesAndCollections(spec, membersByMethod::put); + catalogueActions(spec, membersByMethod::put); + return Collections.unmodifiableMap(membersByMethod); + } + + private static void cataloguePropertiesAndCollections(final ObjectSpecificationBuilder spec, final BiConsumer onMember) { + spec.streamDeclaredAssociations(MixedIn.EXCLUDED) + .forEach(field-> + field.streamFacets(ImperativeFacet.class) + .map(ImperativeFacet::getMethods) + .flatMap(Can::stream) + .map(MethodFacade::asMethodElseFail) // expected regular + .peek(method->_Reflect.guardAgainstSynthetic(method.method())) // expected non-synthetic + .forEach(imperativeFacetMethod->onMember.accept(imperativeFacetMethod, field))); + } + + private static void catalogueActions(final ObjectSpecificationBuilder spec, final BiConsumer onMember) { + spec.streamDeclaredActions(MixedIn.INCLUDED) + .forEach(userAction-> + userAction.streamFacets(ImperativeFacet.class) + .map(ImperativeFacet::getMethods) + .flatMap(Can::stream) + .map(MethodFacade::asMethodForIntrospection) + .peek(method->_Reflect.guardAgainstSynthetic(method.method())) // expected non-synthetic + .forEach(imperativeFacetMethod->onMember.accept(imperativeFacetMethod, userAction))); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixedInMemberFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixedInMemberFactory.java new file mode 100644 index 00000000000..423b4883fe9 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixedInMemberFactory.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.apache.causeway.commons.internal.debug._Debug.Profiler; +import org.apache.causeway.core.metamodel.spec.ActionScope; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import org.apache.causeway.core.metamodel.spec.feature.MixedIn; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; + +record MixedInMemberFactory( + ObjectSpecification spec, + MixinSpecStreamer mixinSpecStreamer) { + + /** + * Creates all mixed in properties and collections for this spec. + * @param profiler + */ + public List createMixedInAssociations(final Profiler profiler) { + + var include = + + profiler.measure("members.mixedInAssociations.createMixedInAssociation.inclusion", ()-> + + spec.isEntityOrViewModelOrAbstract() + && !spec.isValue() + && !spec.isInjectable() + ); + + return include + ? profiler.measure("members.mixedInAssociations.createMixedInAssociation.stream", ()-> + mixinSpecStreamer.streamMixinSpecsFor(spec) + .flatMap(it->createMixedInAssociation(it, profiler)) + .toList()) + : List.of(); + } + + /** + * Creates all mixed in actions for this spec. + */ + public List createMixedInActions() { + var include = spec.isEntityOrViewModelOrAbstract() + || spec.beanSort().isManagedBeanContributing() + // in support of composite value-type constructor mixins + || spec.beanSort().isValue(); + return include + ? mixinSpecStreamer.streamMixinSpecsFor(spec) + .flatMap(this::createMixedInAction) + .toList() + : List.of(); + } + + // -- HELPER + + private Stream createMixedInAssociation(final ObjectSpecification mixinSpec, final Profiler profiler) { + return profiler.measure("members.mixedInAssociations.createMixedInAssociation.create", ()->{ + var mixinFacet = mixinSpec.mixinFacetElseFail(); + return mixinSpec.streamActions(ActionScope.ANY, MixedIn.EXCLUDED) + .filter(_SpecPredicates::isMixedInAssociation) + .map(ObjectActionDefault.class::cast) + .map(mixedInAssociation(spec, mixinSpec, mixinFacet.mainMethodName())); + }); + } + + private Stream createMixedInAction(final ObjectSpecification mixinSpec) { + var mixinFacet = mixinSpec.mixinFacetElseFail(); + // don't mixin Object_ mixins to domain services + if(spec.beanSort().isManagedBeanContributing() + && mixinFacet.isMixinFor(java.lang.Object.class)) + return Stream.empty(); + + return mixinSpec.streamActions(ActionScope.ANY, MixedIn.EXCLUDED) + // value types only support constructor mixins + .filter(this::whenIsValueThenIsAlsoConstructorMixin) + .filter(_SpecPredicates::isMixedInAction) + .map(ObjectActionDefault.class::cast) + .map(mixedInAction(spec, mixinSpec, mixinFacet.mainMethodName())); + } + + /** + * Whether the mixin's main method returns an instance of type equal to the mixee's type. + *

    + * Introduced to support constructor mixins for value-types and + * also to support associated Actions for Action Parameters. + */ + private boolean whenIsValueThenIsAlsoConstructorMixin(final ObjectAction act) { + return spec.beanSort().isValue() + ? Objects.equals(spec, act.getReturnType()) + : true; + } + + private static Function mixedInAction( + final ObjectSpecification mixeeSpec, + final ObjectSpecification mixinSpec, + final String mixinMethodName) { + + return mixinAction -> new ObjectActionMixedIn( + mixinSpec, mixinMethodName, mixinAction, mixeeSpec); + } + + private static Function mixedInAssociation( + final ObjectSpecification mixeeSpec, + final ObjectSpecification mixinSpec, + final String mixinMethodName) { + + return mixinAction -> mixinAction.getReturnType().isSingular() + ? new OneToOneAssociationMixedIn( + mixeeSpec, mixinAction, mixinSpec, mixinMethodName) + : new OneToManyAssociationMixedIn( + mixeeSpec, mixinAction, mixinSpec, mixinMethodName); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationMutable.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamer.java similarity index 57% rename from core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationMutable.java rename to core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamer.java index 9ff20373e97..8950b98914f 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationMutable.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamer.java @@ -18,30 +18,23 @@ */ package org.apache.causeway.core.metamodel.spec.impl; +import java.util.stream.Stream; + import org.apache.causeway.core.metamodel.spec.ObjectSpecification; -public interface ObjectSpecificationMutable extends ObjectSpecification { +@FunctionalInterface +interface MixinSpecStreamer { - enum IntrospectionRequest { - /** - * No introspection, just register the type, that is, create an initial yet empty {@link ObjectSpecification}. - */ - REGISTER, - /** - * Partial introspection, that only includes type-hierarchy but not members. - */ - TYPE_ONLY, - /** - * Full introspection, that includes type-hierarchy and members. - */ - FULL - } + static MixinSpecStreamer EMPTY = Stream::empty; - void introspect(IntrospectionRequest request); + Stream streamMixinSpecs(); - /** - * Adds configuration-gated framework navigation actions during metamodel post-processing. - */ - void synthesizeNavigationActions(); + default Stream streamMixinSpecsFor(final ObjectSpecification mixeeSpec) { + return streamMixinSpecs() + .filter(mixinSpec-> mixinSpec != mixeeSpec) + .filter(mixinSpec-> mixinSpec.mixinFacet() + .map(mixinFacet->mixinFacet.isMixinFor(mixeeSpec.getCorrespondingClass())) + .orElse(false)); + } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamerEager.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamerEager.java new file mode 100644 index 00000000000..2f4014b13e4 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamerEager.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +import java.util.stream.Stream; + +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.commons.internal.collections._Multimaps; +import org.apache.causeway.commons.internal.debug._Debug.Profiler; +import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; + +record MixinSpecStreamerEager( + Can mixinSpecs, + /** key: mixeeClass, value: mixinSpec */ + _Multimaps.ListMultimap, ObjectSpecification> mixinsByMixeeClass, + Profiler profiler) +implements MixinSpecStreamer { + + MixinSpecStreamerEager(final SpecificationLoader specLoader, final CausewayBeanTypeRegistry beanTypeRegistry) { + this(beanTypeRegistry.streamMixinTypes() + .map(specLoader::specForTypeElseFail) + .filter(mixinSpec-> mixinSpec.mixinFacet().isPresent()) + .collect(Can.toCan()), + _Multimaps.newListMultimap(), + Profiler.getInstance()); + streamMixinSpecs() + .forEach(mixinSpec-> + mixinsByMixeeClass.putElement(mixinSpec.mixinFacetElseFail().mixeeType(), mixinSpec)); + } + + @Override + public Stream streamMixinSpecs() { + return mixinSpecs.stream(); + } + + @Override + public Stream streamMixinSpecsFor(final ObjectSpecification mixeeSpec) { + return profiler.measure("MixinSpecStreamerEager", () -> mixeeSpec.streamTypeHierarchyAndInterfaces() + .map(ObjectSpecification::getCorrespondingClass) + .flatMap(mixinsByMixeeClass::streamElements)); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamerOnTheFly.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamerOnTheFly.java new file mode 100644 index 00000000000..9c4b5752987 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamerOnTheFly.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +import java.util.stream.Stream; + +import org.apache.causeway.commons.internal.debug._Debug.Profiler; +import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; + +@Deprecated // not used anymore +record MixinSpecStreamerOnTheFly( + SpecificationLoader specLoader, + CausewayBeanTypeRegistry beanTypeRegistry, + Profiler profiler) +implements MixinSpecStreamer { + + @Deprecated + MixinSpecStreamerOnTheFly( + final SpecificationLoader specLoader, + final CausewayBeanTypeRegistry beanTypeRegistry) { + this(specLoader, beanTypeRegistry, Profiler.getInstance()); + } + + @Deprecated + @Override + public Stream streamMixinSpecs() { + return beanTypeRegistry.streamMixinTypes() + .map(specLoader::specForTypeElseFail); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectMemberContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectMemberContainer.java deleted file mode 100644 index e6420489902..00000000000 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectMemberContainer.java +++ /dev/null @@ -1,133 +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. - */ -package org.apache.causeway.core.metamodel.spec.impl; - -import java.util.Optional; -import java.util.function.Consumer; -import java.util.stream.Stream; - -import org.apache.causeway.commons.collections.ImmutableEnumSet; -import org.apache.causeway.commons.internal.collections._Sets; -import org.apache.causeway.core.metamodel.facetapi.HasFacetHolder; -import org.apache.causeway.core.metamodel.spec.ActionScope; -import org.apache.causeway.core.metamodel.spec.Hierarchical; -import org.apache.causeway.core.metamodel.spec.feature.MixedIn; -import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; -import org.apache.causeway.core.metamodel.spec.feature.ObjectActionContainer; -import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; -import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer; - -/** - * Responsibility: member lookup and streaming with support for inheritance, - * based on access to declared members, super-classes and interfaces. - */ -interface ObjectMemberContainer -extends - HasFacetHolder, - ObjectActionContainer, - ObjectAssociationContainer, - Hierarchical { - - // -- ACTIONS - - @Override - default Optional getAction( - final String id, final ImmutableEnumSet scopes, final MixedIn mixedIn) { - - var declaredAction = getDeclaredAction(id, mixedIn); // no inheritance nor type considered - - if(declaredAction.isPresent()) { - // action found but if its not the right type, stop searching - if(!scopes.contains(declaredAction.get().getScope())) { - return Optional.empty(); - } - return declaredAction; - } - - return isTypeHierarchyRoot() - ? Optional.empty() // stop searching - : superclass().getAction(id, scopes, mixedIn); - } - - @Override - default Stream streamActions( - final ImmutableEnumSet actionTypes, - final MixedIn mixedIn, - final Consumer onActionOverloaded) { - - var actionStream = isTypeHierarchyRoot() - ? streamDeclaredActions(actionTypes, mixedIn) // stop going deeper - : Stream.concat( - streamDeclaredActions(actionTypes, mixedIn), - superclass().streamActions(actionTypes, mixedIn)); - - var actionSignatures = _Sets.newHashSet(); - var actionIds = _Sets.newHashSet(); - - return actionStream - - // as of contributing super-classes same actions might appear more than once (overriding) - .filter(action->{ - if(action.isMixedIn()) { - return true; // do not filter mixedIn actions based on signature - } - var isUnique = actionSignatures - .add(action.getFeatureIdentifier().getMemberNameAndParameterClassNamesIdentityString()); - return isUnique; - }) - - // ensure we don't emit duplicates - .filter(action->{ - var isUnique = actionIds.add(action.getId()); - if(!isUnique) { - onActionOverloaded.accept(action); - } - return isUnique; - }); - } - - // -- ASSOCIATIONS - - @Override - default Optional getAssociation(final String id, final MixedIn mixedIn) { - - var declaredAssociation = getDeclaredAssociation(id, mixedIn); // no inheritance considered - - if(declaredAssociation.isPresent()) return declaredAssociation; - - return isTypeHierarchyRoot() - ? Optional.empty() // stop searching - : superclass().getAssociation(id, mixedIn); - } - - @Override - default Stream streamAssociations(final MixedIn mixedIn) { - - if(isTypeHierarchyRoot()) return streamDeclaredAssociations(mixedIn); // stop going deeper - - var ids = _Sets.newHashSet(); - - return Stream.concat( - streamDeclaredAssociations(mixedIn), - superclass().streamAssociations(mixedIn) - ) - .filter(association->ids.add(association.getId())); // ensure we don't emit duplicates - } - -} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationBuilder.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationBuilder.java new file mode 100644 index 00000000000..dffa8443243 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationBuilder.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +import java.util.Set; + +import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import org.apache.causeway.core.metamodel.spec.feature.ObjectActionContainer; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer; + +interface ObjectSpecificationBuilder +extends + HasSpecificationLoaderInternal, + ObjectActionContainer, + ObjectAssociationContainer, + IntrospectionStateHandler, + ObjectSpecification // TODO remove +// Specification, +// HasLogicalType, +// HasFacetHolder, +// Hierarchical, +// ObjectActionContainer, +// ObjectAssociationContainer, +// ObjectMemberContainer, +// HasSpecificationLoaderInternal + { + + ObjectSpecification build(); + + Set getPotentialOrphans(); + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java index 3e077deb996..fad3f75dbb9 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java @@ -18,67 +18,38 @@ */ package org.apache.causeway.core.metamodel.spec.impl; -import java.lang.reflect.Method; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.Predicate; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -import org.springframework.util.ClassUtils; - import org.apache.causeway.applib.Identifier; -import org.apache.causeway.applib.annotation.Domain; import org.apache.causeway.applib.annotation.DomainObject; import org.apache.causeway.applib.annotation.DomainService; import org.apache.causeway.applib.annotation.Introspection.IntrospectionPolicy; import org.apache.causeway.applib.annotation.ObjectSupport; -import org.apache.causeway.applib.annotation.Where; import org.apache.causeway.applib.fa.FontAwesomeLayers; import org.apache.causeway.applib.id.LogicalType; import org.apache.causeway.applib.services.metamodel.BeanSort; import org.apache.causeway.commons.collections.Can; -import org.apache.causeway.commons.collections.ImmutableEnumSet; import org.apache.causeway.commons.internal.assertions._Assert; -import org.apache.causeway.commons.internal.base._Casts; import org.apache.causeway.commons.internal.base._Lazy; -import org.apache.causeway.commons.internal.base._NullSafe; -import org.apache.causeway.commons.internal.base._Oneshot; import org.apache.causeway.commons.internal.base._Strings; import org.apache.causeway.commons.internal.collections._Lists; -import org.apache.causeway.commons.internal.collections._Maps; -import org.apache.causeway.commons.internal.collections._Multimaps; -import org.apache.causeway.commons.internal.collections._Multimaps.ListMultimap; import org.apache.causeway.commons.internal.collections._Sets; -import org.apache.causeway.commons.internal.collections._Streams; +import org.apache.causeway.commons.internal.debug._Debug.Profiler; import org.apache.causeway.commons.internal.reflection._ClassCache; import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; -import org.apache.causeway.commons.internal.reflection._MethodFacades.MethodFacade; -import org.apache.causeway.commons.internal.reflection._Reflect; import org.apache.causeway.core.config.beans.CausewayBeanMetaData; -import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry; -import org.apache.causeway.core.metamodel.consent.Consent; -import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy; -import org.apache.causeway.core.metamodel.consent.InteractionResult; import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.core.metamodel.facetapi.Facet; import org.apache.causeway.core.metamodel.facetapi.FacetHolder; import org.apache.causeway.core.metamodel.facetapi.FeatureType; -import org.apache.causeway.core.metamodel.facets.FacetedMethod; -import org.apache.causeway.core.metamodel.facets.ImperativeFacet; -import org.apache.causeway.core.metamodel.facets.actions.synthetic.ParentedCollectionNavigationFacet; -import org.apache.causeway.core.metamodel.facets.actions.synthetic.ScalarReferenceNavigationFacet; import org.apache.causeway.core.metamodel.facets.actcoll.typeof.TypeOfFacet; import org.apache.causeway.core.metamodel.facets.all.described.ObjectDescribedFacet; -import org.apache.causeway.core.metamodel.facets.all.help.HelpFacet; import org.apache.causeway.core.metamodel.facets.all.hide.HiddenFacet; import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet; import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacetForStaticMemberName; @@ -93,116 +64,189 @@ import org.apache.causeway.core.metamodel.facets.object.logicaltype.AliasedFacet; import org.apache.causeway.core.metamodel.facets.object.mixin.MixinFacet; import org.apache.causeway.core.metamodel.facets.object.mixin.MixinFacet.Contributing; -import org.apache.causeway.core.metamodel.facets.object.mixin.MixinFacetAbstract; import org.apache.causeway.core.metamodel.facets.object.navparent.NavigableParentFacet; import org.apache.causeway.core.metamodel.facets.object.parented.ParentedCollectionFacet; import org.apache.causeway.core.metamodel.facets.object.title.TitleFacet; import org.apache.causeway.core.metamodel.facets.object.title.TitleRenderRequest; import org.apache.causeway.core.metamodel.facets.object.value.ValueFacet; import org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacet; -import org.apache.causeway.core.metamodel.interactions.InteractionContext; -import org.apache.causeway.core.metamodel.interactions.InteractionUtils; -import org.apache.causeway.core.metamodel.interactions.acc.ObjectTitleContext; -import org.apache.causeway.core.metamodel.interactions.val.ObjectValidityContext; import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.object.ManagedObjects; import org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutorRegistry; import org.apache.causeway.core.metamodel.spec.ActionScope; +import org.apache.causeway.core.metamodel.spec.Hierarchical; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; -import org.apache.causeway.core.metamodel.spec.feature.MixedIn; +import org.apache.causeway.core.metamodel.spec.ObjectSpecificationRecord; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; -import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure; import org.apache.causeway.core.metamodel.spi.EntityTitleSubscriber; import org.apache.causeway.core.metamodel.util.Facets; - -import static org.apache.causeway.commons.internal.base._NullSafe.stream; +import org.jspecify.annotations.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import lombok.Getter; +import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; @Slf4j final class ObjectSpecificationDefault -implements ObjectMemberContainer, ObjectSpecificationMutable, HasSpecificationLoaderInternal { +implements + ObjectSpecificationBuilder, + HasIntrospectionStateHandler, + HasObjectActionContainer, + HasObjectAssociationContainer { // -- CONSTRUCTION - /** - * Lazily built by {@link #getMember(ResolvedMethod)}. - */ - private Map membersByMethod = null; - - private final FacetedMethodsBuilder facetedMethodsBuilder; + private final FacetedMethodsFactory facetedMethodsFactory; private final ClassSubstitutorRegistry classSubstitutorRegistry; - private final _MembersAsColumns columnHelper; + private final _Lazy isInjectableLazy; + private final _Lazy isDomainServiceLazy; + + @Getter(onMethod_ = {@Override}) private final FacetHolder facetHolder; + + @Getter @Accessors(fluent = true) + private final IntrospectionStateHandler introspectionStateHandler; @Getter(onMethod_={@Override}) private final IntrospectionPolicy introspectionPolicy; + @Getter @Accessors(fluent = true) + private final CausewayBeanMetaData typeMeta; + + @Getter @Accessors(fluent = true) + private AssociationContainer objectAssociationContainer = AssociationContainer.EMPTY; + @Getter @Accessors(fluent = true) + private ActionContainer objectActionContainer = ActionContainer.EMPTY; + @Getter @Accessors(fluent = true) + private MemberCatalog memberCatalog = MemberCatalog.EMPTY; + + private final _Lazy> elementSpecification = + _Lazy.threadSafe(()->lookupFacet(TypeOfFacet.class) + .map(TypeOfFacet::elementSpec)); + // -- FIELDS + + private final PostProcessor postProcessor; + + // -- ACTIONS + + /** not API, used for validation */ + @Getter private final Set potentialOrphans = _Sets.newHashSet(); + + // -- INTERFACES + + private final List interfaces = _Lists.newArrayList(); + + // defensive immutable lazy copy of interfaces + private final _Lazy> unmodifiableInterfaces = + _Lazy.threadSafe(()->Can.ofCollection(interfaces)); + + private ObjectSpecification superclassSpec; + + private ValueFacet valueFacet; + private EntityFacet entityFacet; + private ViewModelFacet viewmodelFacet; + private MixinFacet mixinFacet; + private TitleFacet titleFacet; + private IconFacet iconFacet; + private NavigableParentFacet navigableParentFacet; + private AliasedFacet aliasedFacet; + private CssClassFacet cssClassFacet; + + @Getter(lazy = true) + private final Can titleSubscribers = + getServiceRegistry().select(EntityTitleSubscriber.class); + public ObjectSpecificationDefault( final @NonNull CausewayBeanMetaData typeMeta, - final @NonNull MetaModelContext mmc, final @NonNull FacetProcessor facetProcessor, final @NonNull PostProcessor postProcessor, - final @NonNull ClassSubstitutorRegistry classSubstitutorRegistry) { + final @NonNull ClassSubstitutorRegistry classSubstitutorRegistry, + final @NonNull Supplier mixinSpecStreamerSupplier) { - this.correspondingClass = typeMeta.getCorrespondingClass(); - this.logicalType = typeMeta.logicalType(); - this.fullName = correspondingClass.getName(); - this.shortName = typeMeta.logicalType().logicalSimpleName(); - this.beanSort = typeMeta.beanSort(); + final MetaModelContext mmc = facetProcessor.getMetaModelContext(); + + this.typeMeta = typeMeta; + this.isInjectableLazy = _Lazy.threadSafe(()->typeMeta.isInjectable(mmc.getServiceRegistry())); + this.isDomainServiceLazy = _Lazy.threadSafe(()-> + _ClassCache.getInstance().head(getCorrespondingClass()).hasAnnotation(DomainService.class)); this.facetHolder = FacetHolder.simple( - facetProcessor.getMetaModelContext(), - Identifier.classIdentifier(logicalType)); + mmc, + Identifier.classIdentifier(logicalType())); - this.facetProcessor = facetProcessor; this.postProcessor = postProcessor; - - this.isVetoedForInjection = switch (typeMeta.managedBy()) { - case NONE, CAUSEWAY, PERSISTENCE -> true; - case UNSPECIFIED, SPRING -> false; - }; this.classSubstitutorRegistry = classSubstitutorRegistry; // must install EncapsulationFacet (if any) and MemberAnnotationPolicyFacet (if any) facetProcessor.processObjectType(typeMeta.getCorrespondingClass(), this); // naturally supports attribute inheritance from the type's hierarchy - this.introspectionPolicy = this.lookupFacet(IntrospectionPolicyFacet.class) + this.introspectionPolicy = lookupFacet(IntrospectionPolicyFacet.class) .map(IntrospectionPolicyFacet::getIntrospectionPolicy) .orElseGet(()->mmc.getConfiguration().core().metaModel().introspector().policy()); - this.facetedMethodsBuilder = - new FacetedMethodsBuilder(this, facetProcessor, classSubstitutorRegistry); - - this.columnHelper = new _MembersAsColumns(mmc); - } + this.facetedMethodsFactory = + new FacetedMethodsFactory(this, facetProcessor, classSubstitutorRegistry); + + var profiler = Profiler.getInstance(); + + this.introspectionStateHandler = new IntrospectionStateHandlerThreadSafe( + ()->{ + profiler.measure("types", this::introspectTypeHierarchy); + //introspectTypeHierarchy(); + invalidateCachedFacets(); + }, + ()->{ + profiler.measure("members", ()->introspectMembers(mixinSpecStreamerSupplier.get(), profiler)); + //introspectMembers(); +// // make sure we've loaded the facets from layout.xml also. + //Facets.gridPreload(this, null); + profiler.measure("gridPreload", ()->Facets.gridPreload(this, null)); + specLoaderInternal().validateLater(this); + }); + } + + // -- + + @Override public FeatureType getFeatureType() { return FeatureType.OBJECT; } + @Override public BeanSort beanSort() { return typeMeta.beanSort(); } + @Override public Class getCorrespondingClass() { return typeMeta.getCorrespondingClass(); } + @Override public LogicalType logicalType() { return typeMeta.logicalType(); } + @Override public String getFullIdentifier() { return getCorrespondingClass().getName(); } + @Override public String getShortIdentifier() { return logicalType().logicalSimpleName(); } +// @Override public Can getAliases() { return aliases().get(); } + @Override public boolean isDomainService() { return isDomainServiceLazy.get(); } + @Override public boolean isInjectable() { return isInjectableLazy.get(); } + @Override public boolean isParented() { return containsFacet(ParentedCollectionFacet.class); } + @Override public boolean isImmutable() { return containsFacet(ImmutableFacet.class); } + @Override public boolean isHidden() { return containsFacet(HiddenFacet.class); } // -- CONTRACT @Override public int hashCode() { - return correspondingClass.hashCode(); + return getCorrespondingClass().hashCode(); } @Override public boolean equals(final Object o) { return (o instanceof ObjectSpecification other) - ? Objects.equals(this.correspondingClass, other.getCorrespondingClass()) + ? Objects.equals(this.getCorrespondingClass(), other.getCorrespondingClass()) : false; } @Override public String toString() { return "ObjSpec[class=%s, sort=%s, super=%s]" - .formatted(getFullIdentifier(), getBeanSort().name(), superclass() == null + .formatted(getFullIdentifier(), beanSort().name(), superclass() == null ? "Object" : superclass().getFullIdentifier()); } - protected void introspectTypeHierarchy() { + private void introspectTypeHierarchy() { - facetedMethodsBuilder.introspectClass(); + facetedMethodsFactory.introspectClass(); // name addNamedFacetIfRequired(); @@ -219,57 +263,69 @@ protected void introspectTypeHierarchy() { loadSpecOfInterfaces(getCorrespondingClass().getInterfaces()); } - private void introspectMembers() { + private final AtomicBoolean isLockedDown = new AtomicBoolean(); //TODO temporary + private void introspectMembers(final MixinSpecStreamer mixinSpecStreamer, final Profiler profiler) { // yet this logic does not skip UNKNONW - if(this.getBeanSort().isCollection() - || this.getBeanSort().isVetoed() + if(this.beanSort().isCollection() + || this.beanSort().isVetoed() || this.isValue()) { if (log.isDebugEnabled()) { - log.debug("skipping full introspection for {} type {}", this.getBeanSort(), getFullIdentifier()); + log.debug("skipping full introspection for {} type {}", this.beanSort(), getFullIdentifier()); } return; } + Assert.isTrue(!isLockedDown.get(), ()->"object spec for '%s' is in lockdown, because postprocessing already had run (cannot run twice)" + .formatted(getCorrespondingClass().getName())); - // create associations and actions - replaceAssociations(createAssociations()); - replaceActions(createActions()); + // fully introspect up the type hierarchy including interfaces + // because members creation depends on presence of inherited members - postProcessor.postProcess(this); - invalidateCachedFacets(); - } + profiler.measure("hierarchy", ()->{ + streamTypeHierarchyAndInterfaces() + .forEach(it->((IntrospectionStateHandler)it) + .introspectFully()); + }); - @Override - public void synthesizeNavigationActions() { - if (!getMetaModelContext().getConfiguration() - .extensions().commandLog().recordingSupport().isEnabled()) { - return; - } + // create associations and actions - mixedInAssociationAdder.trigger(this::createMixedInAssociationsAndResort); - var existingActionIds = objectActions.stream() - .map(ObjectAction::getId) - .collect(Collectors.toSet()); - var existingSyntheticActionIds = objectActions.stream() - .filter(action -> action.lookupFacet(ParentedCollectionNavigationFacet.class).isPresent() - || action.lookupFacet(ScalarReferenceNavigationFacet.class).isPresent()) - .map(ObjectAction::getId) - .collect(Collectors.toSet()); - var syntheticActions = SyntheticNavigationActionFactory.createFor( - getMetaModelContext(), - this, - associations.stream(), - existingActionIds, - existingSyntheticActionIds) - .toList(); - if (syntheticActions.isEmpty()) { - return; - } + var regularMemberFactory = new RegularMemberFactory(this, facetedMethodsFactory); + var regularAssociations = profiler.measure("members.regularAssociations", ()->regularMemberFactory.createAssociations().toList()); + var regularActions = profiler.measure("members.regularActions", ()->regularMemberFactory.createActions().toList()); + + var mixedInMemberFactory = new MixedInMemberFactory(this, isMixin() + ? MixinSpecStreamer.EMPTY + : mixinSpecStreamer); + var mixedInAssociations = profiler.measure("members.mixedInAssociations", ()->mixedInMemberFactory.createMixedInAssociations(profiler)); + var mixedInActions = profiler.measure("members.mixedInActions", ()->mixedInMemberFactory.createMixedInActions()); + + var syntheticActions = getConfiguration().extensions().commandLog().recordingSupport().isEnabled() + ? new SyntheticNavigationActionFactory(this, regularAssociations, mixedInAssociations, regularActions, mixedInActions).synthesizeNavigationActions() + : List.of(); + + this.objectAssociationContainer = new AssociationContainer( + _MemberSortingUtils.associationsInOrder(this, regularAssociations, mixedInAssociations), + superclass(), + this); + this.objectActionContainer = new ActionContainer( + _MemberSortingUtils.actionsInOrder(this, regularActions, mixedInActions, syntheticActions), + ActionScope.forEnvironment(getMetaModelContext().getSystemEnvironment()), + superclass()); + + //TODO would allow to introspect mixins in isolation if(!isMixin()) { + profiler.measure("members.postProcessor", ()->{ + postProcessor.postProcess(this); + }); + //} + this.memberCatalog = new MemberCatalog(this); + + invalidateCachedFacets(); - replaceActions(Stream.concat(objectActions.stream(), syntheticActions.stream())); - membersByMethod = null; + isLockedDown.set(true); } + //TODO this is a facet factory responsibility + @Deprecated private void addNamedFacetIfRequired() { if (getFacet(MemberNamedFacet.class) == null) { addFacet(new MemberNamedFacetForStaticMemberName( @@ -278,353 +334,20 @@ private void addNamedFacetIfRequired() { } } - // -- create associations and actions - private Stream createAssociations() { - return facetedMethodsBuilder.getAssociationFacetedMethods() - .stream() - .map(this::createAssociation) - .filter(_NullSafe::isPresent); - } - - private ObjectAssociation createAssociation(final FacetedMethod facetMethod) { - if (facetMethod.featureType().isCollection()) - return OneToManyAssociationDefault.forMethod(facetMethod); - else if (facetMethod.featureType().isProperty()) - return OneToOneAssociationDefault.forMethod(facetMethod); - else - return null; - } - - private Stream createActions() { - return facetedMethodsBuilder.getActionFacetedMethods() - .stream() - .map(this::createAction) - .filter(_NullSafe::isPresent); - } - - private ObjectAction createAction(final FacetedMethod facetedMethod) { - if (facetedMethod.featureType().isAction()) { - /* Assuming, that facetedMethod was already populated with ContributingFacet, - * we copy the mixin-sort information from the FacetedMethod to the MixinFacet - * that is held by the mixin's type spec. */ - mixinFacet() - .flatMap(mixinFacet->_Casts.castTo(MixinFacetAbstract.class, mixinFacet)) - .ifPresent(mixinFacetAbstract-> - mixinFacetAbstract.initMixinSortFrom(facetedMethod)); - - return this.isMixin() - ? ObjectActionDefault.forMixinMain(facetedMethod) - : ObjectActionDefault.forMethod(facetedMethod); - } else - return null; - } - - // -- getObjectAction - - @Override - public Optional getDeclaredAction( - final @Nullable String id, - final ImmutableEnumSet actionScopes, - final MixedIn mixedIn) { - - introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, - ()->"getDeclaredAction %s on %s".formatted(id, this.getFeatureIdentifier())); - - return _Strings.isEmpty(id) - ? Optional.empty() - : streamDeclaredActions(actionScopes, mixedIn) - .filter(action-> - id.equals(action.getFeatureIdentifier().getMemberNameAndParameterClassNamesIdentityString()) - || id.equals(action.getFeatureIdentifier().memberLogicalName()) - ) - .findFirst(); - } - @Override public Optional getMember(final ResolvedMethod method) { - introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, - ()->"getMember %s on %s".formatted(method.name(), this.getFeatureIdentifier())); - - if (membersByMethod == null) { - this.membersByMethod = catalogueMembers(); - } - - var member = membersByMethod.get(method); - return Optional.ofNullable(member); - } - - private Map catalogueMembers() { - var membersByMethod = _Maps.newHashMap(); - cataloguePropertiesAndCollections(membersByMethod::put); - catalogueActions(membersByMethod::put); - return membersByMethod; - } - - private void cataloguePropertiesAndCollections(final BiConsumer onMember) { - streamDeclaredAssociations(MixedIn.EXCLUDED) - .forEach(field-> - field.streamFacets(ImperativeFacet.class) - .map(ImperativeFacet::getMethods) - .flatMap(Can::stream) - .map(MethodFacade::asMethodElseFail) // expected regular - .peek(method->_Reflect.guardAgainstSynthetic(method.method())) // expected non-synthetic - .forEach(imperativeFacetMethod->onMember.accept(imperativeFacetMethod, field))); - } - - private void catalogueActions(final BiConsumer onMember) { - streamDeclaredActions(MixedIn.INCLUDED) - .forEach(userAction-> - userAction.streamFacets(ImperativeFacet.class) - .map(ImperativeFacet::getMethods) - .flatMap(Can::stream) - .map(MethodFacade::asMethodForIntrospection) - .peek(method->_Reflect.guardAgainstSynthetic(method.method())) // expected non-synthetic - .forEach(imperativeFacetMethod-> - onMember.accept(imperativeFacetMethod, userAction))); + introspectFully(); + return memberCatalog.lookupMember(method); } // -- ELEMENT SPECIFICATION - private final _Lazy> elementSpecification = - _Lazy.threadSafe(()->lookupFacet(TypeOfFacet.class) - .map(TypeOfFacet::elementSpec)); - @Override public Optional explicitElementSpec() { return elementSpecification.get(); } - // -- TABLE COLUMN RENDERING - - @Override public Stream streamAssociationsForColumnRendering(final ColumnQuery columnQuery) { - return columnHelper.streamAssociationsForColumnRendering(this, columnQuery); - } - - @Override - public Stream streamActionsForColumnRendering(final Where where) { - return columnHelper.streamActionsForColumnRendering(this, where); - } - - // -- DETERMINE INJECTABILITY - - private boolean isVetoedForInjection; - - private final _Lazy isInjectableLazy = _Lazy.threadSafe(()-> - !isVetoedForInjection - && !getBeanSort().isAbstract() - && !getBeanSort().isValue() - && !getBeanSort().isEntity() - && !getBeanSort().isViewModel() - && !getBeanSort().isMixin() - && (getBeanSort().isManagedBeanAny() - || getServiceRegistry() - .lookupRegisteredBeanById(logicalType()) - .isPresent()) - ); - - @Override - public boolean isInjectable() { - return isInjectableLazy.get(); - } - - private _Lazy isDomainServiceLazy = _Lazy.threadSafe(()-> - _ClassCache.getInstance().head(getCorrespondingClass()).hasAnnotation(DomainService.class)); - - @Override - public boolean isDomainService() { - return isDomainServiceLazy.get(); - } - - //----------------------------------------------------------------------------------------------------------------- - // MERGED FROM FORMER ObjectSpecificationAbstract - //----------------------------------------------------------------------------------------------------------------- - - // -- FIELDS - - private final PostProcessor postProcessor; - private final FacetProcessor facetProcessor; - - @Getter private final BeanSort beanSort; - - // -- ASSOCIATIONS - - private final List associations = _Lists.newArrayList(); - - // defensive immutable lazy copy of associations - private final _Lazy> unmodifiableAssociations = - _Lazy.threadSafe(()->Can.ofCollection(associations)); - - // -- ACTIONS - - private final List objectActions = _Lists.newArrayList(); - - /** not API, used for validation */ - @Getter private final Set potentialOrphans = _Sets.newHashSet(); - - // defensive immutable lazy copy of objectActions - private final _Lazy> unmodifiableActions = - _Lazy.threadSafe(()->Can.ofCollection(objectActions)); - - // partitions and caches objectActions by type; updated in sortCacheAndUpdateActions() - private final ListMultimap objectActionsByType = - _Multimaps.newConcurrentListMultimap(); - - // -- INTERFACES - - private final List interfaces = _Lists.newArrayList(); - - // defensive immutable lazy copy of interfaces - private final _Lazy> unmodifiableInterfaces = - _Lazy.threadSafe(()->Can.ofCollection(interfaces)); - - //private final Subclasses directSubclasses = new Subclasses(); - // built lazily - //private Subclasses transitiveSubclasses; - - private final Class correspondingClass; - private final String fullName; - private final String shortName; - - private final LogicalType logicalType; - - private ObjectSpecification superclassSpec; - - private ValueFacet valueFacet; - private EntityFacet entityFacet; - private ViewModelFacet viewmodelFacet; - private MixinFacet mixinFacet; - private TitleFacet titleFacet; - private IconFacet iconFacet; - private NavigableParentFacet navigableParentFacet; - private AliasedFacet aliasedFacet; - private CssClassFacet cssClassFacet; - - private IntrospectionState introspectionState = IntrospectionState.NOT_INTROSPECTED; - - @Getter(onMethod_ = {@Override}) private FacetHolder facetHolder; - - // -- Stuff immediately derivable from class - @Override - public final FeatureType getFeatureType() { - return FeatureType.OBJECT; - } - - @Override - public final LogicalType logicalType() { - return logicalType; - } - - @Override - public final Class getCorrespondingClass() { - return correspondingClass; - } - - @Override - public final String getShortIdentifier() { - return shortName; - } - - /** - * The {@link Class#getName() (full) name} of the - * {@link #getCorrespondingClass() class}. - */ - @Override - public final String getFullIdentifier() { - return fullName; - } - - @Override - public void introspect(final IntrospectionRequest request) { - switch (request) { - case REGISTER -> introspectUpTo(IntrospectionState.NOT_INTROSPECTED, - ()->"introspect(%s)".formatted(request)); - case TYPE_ONLY -> introspectUpTo(IntrospectionState.TYPE_INTROSPECTED, - ()->"introspect(%s)".formatted(request)); - case FULL -> introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, - ()->"introspect(%s)".formatted(request)); - } - } - - enum IntrospectionState { - /** - * At this stage, {@link LogicalType} only. - */ - NOT_INTROSPECTED, - /** - * Interim stage, to avoid infinite loops while on way to being {@link #TYPE_INTROSPECTED} - */ - TYPE_BEING_INTROSPECTED, - /** - * Type has been introspected (but not its members). - */ - TYPE_INTROSPECTED, - /** - * Interim stage, to avoid infinite loops while on way to being {@link #FULLY_INTROSPECTED} - */ - MEMBERS_BEING_INTROSPECTED, - /** - * Fully introspected... class and also its members. - */ - FULLY_INTROSPECTED - } - - /** - * @param introspectionContextProvider keeps track of the causal chain of introspection requests - */ - private void introspectUpTo(final IntrospectionState upTo, final Supplier introspectionContextProvider) { - if(!isLessThan(upTo)) - return; // optimization - - if(log.isDebugEnabled()) { - log.debug("introspectingUpTo: {}, {}", getFullIdentifier(), upTo); - } - - switch (introspectionState) { - case NOT_INTROSPECTED->{ - if(isLessThan(upTo)) { - introspectType(); - } - if(isLessThan(upTo)) { - introspectFully(); - specLoaderInternal().validateLater(this, introspectionContextProvider); - } - } - case TYPE_BEING_INTROSPECTED->{} // nothing to do (interim state during introspectType) - case TYPE_INTROSPECTED->{ - if(isLessThan(upTo)) { - introspectFully(); - specLoaderInternal().validateLater(this, introspectionContextProvider); - } - } - case MEMBERS_BEING_INTROSPECTED->{}// nothing to do (interim state during introspect fully) - case FULLY_INTROSPECTED->{}// nothing to do ... all done - } - } - - private void introspectType() { - // set to avoid infinite loops - this.introspectionState = IntrospectionState.TYPE_BEING_INTROSPECTED; - introspectTypeHierarchy(); - invalidateCachedFacets(); - this.introspectionState = IntrospectionState.TYPE_INTROSPECTED; - } - - private void introspectFully() { - - // set to avoid infinite loops - this.introspectionState = IntrospectionState.MEMBERS_BEING_INTROSPECTED; - introspectMembers(); - this.introspectionState = IntrospectionState.FULLY_INTROSPECTED; - - // make sure we've loaded the facets from layout.xml also. - Facets.gridPreload(this, null); - } - - private boolean isLessThan(final IntrospectionState upTo) { - return this.introspectionState.compareTo(upTo) < 0; - } - - protected void loadSpecOfSuperclass(final Class superclass) { + private void loadSpecOfSuperclass(final Class superclass) { if (superclass == null) return; @@ -635,7 +358,7 @@ protected void loadSpecOfSuperclass(final Class superclass) { } } - protected void loadSpecOfInterfaces(final Class[] interfaces) { + private void loadSpecOfInterfaces(final Class[] interfaces) { if(interfaces==null) return; @@ -687,34 +410,7 @@ protected void loadSpecOfInterfaces(final Class[] interfaces) { } } - protected final void replaceAssociations(final Stream associations) { - var orderedAssociations = _MemberSortingUtils.sortAssociationsIntoList(associations); - synchronized (unmodifiableAssociations) { - this.associations.clear(); - this.associations.addAll(orderedAssociations); - unmodifiableAssociations.clear(); // invalidate - } - } - - protected final void replaceActions(final Stream objectActions) { - var orderedActions = _MemberSortingUtils.sortActionsIntoList(objectActions); - synchronized (unmodifiableActions){ - this.objectActions.clear(); - this.objectActions.addAll(orderedActions); - unmodifiableActions.clear(); // invalidate - - // rebuild objectActionsByType multi-map - for (var actionType : ActionScope.values()) { - var objectActionForType = objectActionsByType.getOrElseNew(actionType); - objectActionForType.clear(); - orderedActions.stream() - .filter(ObjectAction.Predicates.ofActionType(actionType)) - .forEach(objectActionForType::add); - } - } - } - - void invalidateCachedFacets() { + private void invalidateCachedFacets() { this.valueFacet = getFacet(ValueFacet.class); this.titleFacet = lookupNonFallbackFacet(TitleFacet.class).orElse(null); this.iconFacet = getFacet(IconFacet.class); @@ -726,7 +422,7 @@ void invalidateCachedFacets() { @Override public final Optional> valueFacet() { if(valueFacet == null - && getBeanSort().isValue()) { + && beanSort().isValue()) { invalidateCachedFacets(); } return Optional.ofNullable(valueFacet); @@ -764,7 +460,7 @@ public String getTitle(final TitleRenderRequest titleRenderRequest) { if (titleFacet != null) { var titleString = titleFacet.title(titleRenderRequest); if (!_Strings.isEmpty(titleString)) { - notifySubscribersIfEntity(titleRenderRequest, titleString); + notifyAnyTitleSubscribers(titleRenderRequest, titleString); return titleString; } } @@ -774,18 +470,6 @@ public String getTitle(final TitleRenderRequest titleRenderRequest) { return prefix + getSingularName(); } - private void notifySubscribersIfEntity( - final TitleRenderRequest titleRenderRequest, - final String titleString) { - if (!isEntity()) - return; - - var managedObject = titleRenderRequest.object(); - managedObject.getBookmark().ifPresent(bookmark -> { - getTitleSubscribers().stream().forEach(x -> x.entityTitleIs(bookmark, titleString)); - }); - } - @Override public Object getNavigableParent(final Object object) { return navigableParentFacet != null @@ -801,7 +485,7 @@ public String getCssClass(final ManagedObject reference) { } @Override - public Can getAliases() { + public Can aliases() { return aliasedFacet != null ? aliasedFacet.getAliases() : Can.empty(); @@ -833,7 +517,6 @@ private Optional faLayers(final ManagedObject domainObject){ @Override public boolean isOfType(final ObjectSpecification other) { - var thisClass = this.getCorrespondingClass(); var otherClass = other.getCorrespondingClass(); @@ -843,7 +526,6 @@ public boolean isOfType(final ObjectSpecification other) { @Override public boolean isOfTypeResolvePrimitive(final ObjectSpecification other) { - var thisClass = ClassUtils.resolvePrimitiveIfNecessary(this.getCorrespondingClass()); var otherClass = ClassUtils.resolvePrimitiveIfNecessary(other.getCorrespondingClass()); @@ -874,16 +556,6 @@ public String getDescription() { .orElse(""); } - /* - * help is typically a reference (eg a URL) and so should not default to a - * textual value if not set up - */ - @Override - public String getHelp() { - var helpFacet = getFacet(HelpFacet.class); - return helpFacet == null ? null : helpFacet.value(); - } - @Override public final Optional contributing() { return mixinFacet() @@ -893,61 +565,12 @@ public final Optional contributing() { // -- FACET HANDLING @Override - public Q getFacet(final Class facetType) { - + public Optional lookupFacet(final Class facetType) { synchronized(unmodifiableInterfaces) { - - // lookup facet holder's facet - var facets1 = _NullSafe.streamNullable(facetHolder.getFacet(facetType)); - - // lookup all interfaces - var facets2 = _NullSafe.stream(interfaces()) - .filter(_NullSafe::isPresent) // just in case - .map(interfaceSpec->interfaceSpec.getFacet(facetType)); - - // search up the inheritance hierarchy - var facets3 = _NullSafe.streamNullable(superclass()) - .map(superSpec->superSpec.getFacet(facetType)); - - var facetsCombined = _Streams.concat(facets1, facets2, facets3); - - var notANoopFacetFilter = new NotANoopFacetFilter(); - - return facetsCombined - .filter(notANoopFacetFilter) - .findFirst() - .orElse(notANoopFacetFilter.noopFacet); - + return Hierarchical.lookupFacet(facetType, facetHolder, this); } } - @Domain.Exclude - private static class NotANoopFacetFilter implements Predicate { - Q noopFacet; - - @Override - public boolean test(final Q facet) { - if(facet==null) - return false; - if(!facet.precedence().isFallback()) - return true; - if(noopFacet == null) { - noopFacet = facet; - } - return false; - } - } - - @Override - public ObjectTitleContext createTitleInteractionContext( - final ManagedObject targetObjectAdapter, - final InteractionInitiatedBy interactionMethod) { - - return new ObjectTitleContext(targetObjectAdapter, getFeatureIdentifier(), - targetObjectAdapter.getTitle(), - interactionMethod); - } - // -- INHERITED @Override @@ -960,25 +583,9 @@ public Can interfaces() { return unmodifiableInterfaces.get(); } - // -- ASSOCIATIONS - - @Override - public Stream streamDeclaredAssociations(final MixedIn mixedIn) { - introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, - ()->"streamDeclaredAssociations of %s".formatted(this.getFeatureIdentifier())); - - mixedInAssociationAdder.trigger(this::createMixedInAssociationsAndResort); // only if not already - - synchronized(unmodifiableAssociations) { - return stream(unmodifiableAssociations.get()) - .filter(mixedIn.toFilter()); - } - } - @Override public Optional getMember(final String memberId) { - introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, - ()->"getMember %s of %s".formatted(memberId, this.getFeatureIdentifier())); + introspectFully(); if(_Strings.isEmpty(memberId)) return Optional.empty(); @@ -994,229 +601,52 @@ public Optional getMember(final String memberId) { return Optional.empty(); } - @Override - public Optional getDeclaredAssociation(final String id, final MixedIn mixedIn) { - introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, - ()->"getDeclaredAssociation %s of %s".formatted(id, this.getFeatureIdentifier())); - - if(_Strings.isEmpty(id)) - return Optional.empty(); - - return streamDeclaredAssociations(mixedIn) - .filter(objectAssociation->objectAssociation.getId().equals(id)) - .findFirst(); - } - - @Override - public Stream streamRuntimeActions(final MixedIn mixedIn) { - var actionScopes = ActionScope.forEnvironment(getMetaModelContext().getSystemEnvironment()); - return streamActions(actionScopes, mixedIn); - } - - @Override - public Stream streamDeclaredActions( - final ImmutableEnumSet actionScopes, - final MixedIn mixedIn) { - introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, - ()->"streamDeclaredActions of %s".formatted(this.getFeatureIdentifier())); - - mixedInActionAdder.trigger(this::createMixedInActionsAndResort); - - return actionScopes.stream() - .flatMap(actionScope->stream(objectActionsByType.get(actionScope))) - .filter(mixedIn.toFilter()); - } - - // -- mixin associations (properties and collections) - /** - * Creates all mixed in properties and collections for this spec. - */ - private Stream createMixedInAssociations() { - if (isInjectable() || isValue()) - return Stream.empty(); - return getCausewayBeanTypeRegistry().streamMixinTypes() - .flatMap(this::createMixedInAssociation); - } - - private Stream createMixedInAssociation(final Class mixinType) { - var mixinSpec = specLoaderInternal().loadSpecification(mixinType, - IntrospectionRequest.FULL); - if (mixinSpec == null - || mixinSpec == this) - return Stream.empty(); - var mixinFacet = mixinSpec.mixinFacet().orElse(null); - if(mixinFacet == null) - // this shouldn't happen; to be covered by meta-model validation later - return Stream.empty(); - if(!mixinFacet.isMixinFor(getCorrespondingClass())) - return Stream.empty(); - var mixinMethodName = mixinFacet.getMainMethodName(); - - return mixinSpec.streamActions(ActionScope.ANY, MixedIn.EXCLUDED) - .filter(_SpecPredicates::isMixedInAssociation) - .map(ObjectActionDefault.class::cast) - .map(_MixedInMemberFactory.mixedInAssociation(this, mixinSpec, mixinMethodName)) - .peek(facetProcessor::processMemberOrder); - } - - // -- mixin actions - /** - * Creates all mixed in actions for this spec. - */ - private Stream createMixedInActions() { - return getCausewayBeanTypeRegistry().streamMixinTypes() - .flatMap(this::createMixedInAction); - } - - private Stream createMixedInAction(final Class mixinType) { - - var mixinSpec = specLoaderInternal().loadSpecification(mixinType, - IntrospectionRequest.FULL); - if (mixinSpec == null - || mixinSpec == this) - return Stream.empty(); - var mixinFacet = mixinSpec.mixinFacet().orElse(null); - if(mixinFacet == null) - // this shouldn't happen; to be covered by meta-model validation later - return Stream.empty(); - if(!mixinFacet.isMixinFor(getCorrespondingClass())) - return Stream.empty(); - // don't mixin Object_ mixins to domain services - if(getBeanSort().isManagedBeanContributing() - && mixinFacet.isMixinFor(java.lang.Object.class)) - return Stream.empty(); - - var mixinMethodName = mixinFacet.getMainMethodName(); - - return mixinSpec.streamActions(ActionScope.ANY, MixedIn.EXCLUDED) - // value types only support constructor mixins - .filter(this::whenIsValueThenIsAlsoConstructorMixin) - .filter(_SpecPredicates::isMixedInAction) - .map(ObjectActionDefault.class::cast) - .map(_MixedInMemberFactory.mixedInAction(this, mixinSpec, mixinMethodName)) - .peek(facetProcessor::processMemberOrder); - } - - /** - * Whether the mixin's main method returns an instance of type equal to the mixee's type. - *

    - * Introduced to support constructor mixins for value-types and - * also to support associated Actions for Action Parameters. - */ - private boolean whenIsValueThenIsAlsoConstructorMixin(final ObjectAction act) { - return getBeanSort().isValue() - ? Objects.equals(this, act.getReturnType()) - : true; + // -- SHALLOW IMMUTABLE / EXPERIMENTAL + + @Override + public ObjectSpecificationRecord build() { + //WIP + return new ObjectSpecificationRecord( + typeMeta, + getFeatureType(), + facetHolder, + this,//Hierarchical, + objectActionContainer, + objectAssociationContainer, + getServiceRegistry().select(EntityTitleSubscriber.class), + introspectionPolicy, + aliases(), + valueFacet(), + entityFacet(), + viewmodelFacet(), + mixinFacet(), + lookupFacet(ObjectNamedFacet.class), + lookupFacet(ObjectDescribedFacet.class), + lookupFacet(TypeOfFacet.class), + lookupNonFallbackFacet(TitleFacet.class), + lookupFacet(IconFacet.class), + lookupFacet(FaFacet.class), + lookupFacet(NavigableParentFacet.class), + lookupFacet(CssClassFacet.class), + isDomainService(), + isInjectable(), + isParented(), + isImmutable(), + isHidden(), + new MemberCatalog(this).membersByMethod()); + } + + // -- HELPER + + private void notifyAnyTitleSubscribers( + final TitleRenderRequest titleRenderRequest, + final String titleString) { + if (!isEntity()) + return; + + var managedObject = titleRenderRequest.object(); + managedObject.getBookmark().ifPresent(bookmark -> { + getTitleSubscribers().stream().forEach(x -> x.entityTitleIs(bookmark, titleString)); + }); } - - // -- VALIDITY - - @Override - public Consent isValid( - final ManagedObject targetAdapter, - final InteractionInitiatedBy interactionInitiatedBy) { - - return isValidResult(targetAdapter, interactionInitiatedBy).createConsent(); - } - - @Override - public InteractionResult isValidResult( - final ManagedObject targetAdapter, - final InteractionInitiatedBy interactionInitiatedBy) { - var validityContext = - createValidityInteractionContext( - targetAdapter, interactionInitiatedBy); - return InteractionUtils.isValidResult(this, validityContext); - } - - /** - * Create an {@link InteractionContext} representing an attempt to save the - * object. - */ - @Override - public ObjectValidityContext createValidityInteractionContext( - final ManagedObject targetAdapter, final InteractionInitiatedBy interactionInitiatedBy) { - return new ObjectValidityContext(targetAdapter, getFeatureIdentifier(), interactionInitiatedBy); - } - - // -- convenience isXxx (looked up from facets) - @Override - public boolean isImmutable() { - return containsFacet(ImmutableFacet.class); - } - - @Override - public boolean isHidden() { - return containsFacet(HiddenFacet.class); - } - - @Override - public boolean isParented() { - return containsFacet(ParentedCollectionFacet.class); - } - - // -- MIXIN ADDER ONESHOTs - - private final _Oneshot mixedInActionAdder = new _Oneshot(); - private final _Oneshot mixedInAssociationAdder = new _Oneshot(); - - /** - * one-shot: must be no-op, if already created - */ - private void createMixedInActionsAndResort() { - var include = isEntityOrViewModelOrAbstract() - || getBeanSort().isManagedBeanContributing() - // in support of composite value-type constructor mixins - || getBeanSort().isValue(); - if(!include) - return; - var mixedInActions = createMixedInActions() - .collect(Collectors.toList()); - if(mixedInActions.isEmpty()) - return; // nothing to do (this spec has no mixed-in actions, regular actions have already been added) - - var regularActions = _Lists.newArrayList(objectActions); // defensive copy - - // note: we are doing this before any member sorting - _MemberIdClashReporting.flagAnyMemberIdClashes(this, regularActions, mixedInActions); - - replaceActions(Stream.concat( - regularActions.stream(), - mixedInActions.stream())); - } - - /** - * one-shot: must be no-op, if already created - */ - private void createMixedInAssociationsAndResort() { - if(!isEntityOrViewModelOrAbstract()) - return; - var mixedInAssociations = createMixedInAssociations() - .collect(Collectors.toList()); - if(mixedInAssociations.isEmpty()) - return; // nothing to do (this spec has no mixed-in associations, regular associations have already been added) - - var regularAssociations = _Lists.newArrayList(associations); // defensive copy - - // note: we are doing this before any member sorting - _MemberIdClashReporting.flagAnyMemberIdClashes(this, regularAssociations, mixedInAssociations); - - replaceAssociations(Stream.concat( - regularAssociations.stream(), - mixedInAssociations.stream())); - } - - @Getter(lazy = true) - private final CausewayBeanTypeRegistry causewayBeanTypeRegistry = - getServiceRegistry() - .lookupServiceElseFail(CausewayBeanTypeRegistry.class); - - @Getter(lazy = true) - private final Can titleSubscribers = - getServiceRegistry().select(EntityTitleSubscriber.class); - - boolean isFullyIntrospected() { - return this.introspectionState == IntrospectionState.FULLY_INTROSPECTED; - } - } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/PostProcessor.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/PostProcessor.java index 9b7fcdfe4a8..dbbaf7e5c7a 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/PostProcessor.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/PostProcessor.java @@ -21,7 +21,6 @@ import org.apache.causeway.commons.collections.Can; import org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessor; import org.apache.causeway.core.metamodel.progmodel.ProgrammingModel; -import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.MixedIn; record PostProcessor( @@ -34,28 +33,31 @@ public PostProcessor(final ProgrammingModel programmingModel) { .collect(Can.toCan())); } - public void postProcess(final ObjectSpecification objectSpecification) { + public void postProcess(final ObjectSpecificationBuilder objSpecBuilder) { for (var postProcessor : enabledPostProcessors) { - if(!postProcessor.getFilter().test(objectSpecification)) continue; + if(!postProcessor.getFilter().test(objSpecBuilder)) { + continue; + } - postProcessor.postProcessObject(objectSpecification); + postProcessor.postProcessObject(objSpecBuilder); - objectSpecification.streamRuntimeActions(MixedIn.INCLUDED) + objSpecBuilder.streamRuntimeActions(MixedIn.INCLUDED) .forEach(act->{ act.streamParameters().forEach(param -> - postProcessor.postProcessParameter(objectSpecification, act, param)); - postProcessor.postProcessAction(objectSpecification, act); + postProcessor.postProcessParameter(objSpecBuilder, act, param)); + postProcessor.postProcessAction(objSpecBuilder, act); }); - objectSpecification.streamProperties(MixedIn.INCLUDED) - .forEach(prop->postProcessor.postProcessProperty(objectSpecification, prop)); + objSpecBuilder.streamProperties(MixedIn.INCLUDED) + .forEach(prop->postProcessor.postProcessProperty(objSpecBuilder, prop)); - objectSpecification.streamCollections(MixedIn.INCLUDED) - .forEach(coll->postProcessor.postProcessCollection(objectSpecification, coll)); + objSpecBuilder.streamCollections(MixedIn.INCLUDED) + .forEach(coll->postProcessor.postProcessCollection(objSpecBuilder, coll)); } + } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ProgrammingModelDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ProgrammingModelDefault.java index 76117882d58..5ce2f3c7c2f 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ProgrammingModelDefault.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ProgrammingModelDefault.java @@ -92,7 +92,6 @@ import org.apache.causeway.core.metamodel.postprocessors.allbutparam.authorization.AuthorizationPostProcessor; import org.apache.causeway.core.metamodel.postprocessors.members.SynthesizeDomainEventsForMixinPostProcessor; import org.apache.causeway.core.metamodel.postprocessors.members.navigation.NavigationFacetFromHiddenTypePostProcessor; -import org.apache.causeway.core.metamodel.postprocessors.members.navigation.SynthesizeNavigationActionsPostProcessor; import org.apache.causeway.core.metamodel.postprocessors.object.ProjectionFacetsPostProcessor; import org.apache.causeway.core.metamodel.postprocessors.param.ChoicesAndDefaultsPostProcessor; import org.apache.causeway.core.metamodel.postprocessors.param.TypicalLengthFromTypePostProcessor; @@ -250,9 +249,6 @@ private void addFacetFactories() { private void addPostProcessors() { var mmc = getMetaModelContext(); - // must run before later action post-processors inspect the action list - addPostProcessor(PostProcessingOrder.A0_BEFORE_BUILTIN, new SynthesizeNavigationActionsPostProcessor(mmc)); - // must run before Object nouns are used addPostProcessor(PostProcessingOrder.A1_BUILTIN, new SynthesizeObjectNamingPostProcessor(mmc)); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/RegularMemberFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/RegularMemberFactory.java new file mode 100644 index 00000000000..0f7911e82a1 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/RegularMemberFactory.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.spec.impl; + +import java.util.stream.Stream; + +import org.apache.causeway.commons.internal.base._Casts; +import org.apache.causeway.commons.internal.base._NullSafe; +import org.apache.causeway.core.metamodel.facets.FacetedMethod; +import org.apache.causeway.core.metamodel.facets.object.mixin.MixinFacetAbstract; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; + +record RegularMemberFactory( + ObjectSpecificationBuilder spec, + FacetedMethodsFactory factory) { + + Stream createAssociations() { + return factory.createAssociationFacetedMethods() + .map(this::createAssociation) + .filter(_NullSafe::isPresent); + } + + Stream createActions() { + return factory.createActionFacetedMethods() + .map(this::createAction) + .filter(_NullSafe::isPresent); + } + + // -- HELPER + + private ObjectAssociation createAssociation(final FacetedMethod facetMethod) { + if (facetMethod.featureType().isCollection()) + return OneToManyAssociationDefault.forMethod(facetMethod); + else if (facetMethod.featureType().isProperty()) + return OneToOneAssociationDefault.forMethod(facetMethod); + else + return null; + } + + private ObjectAction createAction(final FacetedMethod facetedMethod) { + if (facetedMethod.featureType().isAction()) { + /* Assuming, that facetedMethod was already populated with ContributingFacet, + * we copy the mixin-sort information from the FacetedMethod to the MixinFacet + * that is held by the mixin's type spec. */ + spec.mixinFacet() + .flatMap(mixinFacet->_Casts.castTo(MixinFacetAbstract.class, mixinFacet)) + .ifPresent(mixinFacetAbstract-> + mixinFacetAbstract.initMixinSortFrom(facetedMethod)); + + return spec.isMixin() + ? ObjectActionDefault.forMixinMain(facetedMethod) + : ObjectActionDefault.forMethod(facetedMethod); + } else + return null; + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecLoadingDevNotes.adoc b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecLoadingDevNotes.adoc new file mode 100644 index 00000000000..2c05811937c --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecLoadingDevNotes.adoc @@ -0,0 +1,94 @@ += Object Specification Loading + +:Notice: 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. + +== Participants + +- SpecificationLoader +- ObjectSpecification +- FacetedMethodsBuilder +- FacetProcessor +- PostProcessor +- ClassSubstitutorRegistry/ClassSubstitutor + +== SpecificationLoader + +This class acts as the primary engine for constructing the system's metadata (the "Metamodel"). Based on the requirements and implementation notes: + +- *Orchestration*: It integrates various subsystems, such as mix-in processing, class substitution, and post-processing. +- *Configuration Management*: It handles different operational modes (EAGER vs. LAZY) to determine how much of the metamodel is explored upfront versus on-the-fly. +- *Lifecycle & Reloading*: It manages the complexity of "reloading" specifications while ensuring that shared references do not cause consistency issues. + +[source,java] +.SpecificationLoader +---- +ObjectSpecification loadSpecification(Class cls) +---- + +== ObjectSpecification + +This is the core data structure representing a class's meta data within the _Metamodel_. + +- *Stateful Wrapper*: It is designed as a wrapper around internal, potentially mutable data. This allows the system to maintain consistent references even when underlying information is refreshed or updated. +- *FacetHolder*: It holds various facets of the class definition +- *ObjectActionContainer* and *ObjectAssociationContainer* class member meta data. + +[source,java] +.ObjectSpecification +---- +// inherited from Hierarchical ... +Can interfaces(); +boolean isOfType(ObjectSpecification other); +boolean isOfTypeResolvePrimitive(ObjectSpecification other); +ObjectSpecification superclass(); + +FacetHolder getFacetHolder(); // inherited from HasFacetHolder +FeatureType getFeatureType(); // inherited from Specification +LogicalType logicalType(); // inherited from HasLogicalType +ObjectActionContainer objectActionContainer(); // inherited from HasObjectActionContainer +ObjectAssociationContainer objectAssociationContainer(); // inherited from HasObjectAssociationContainer +---- + +== Building the Metamodel + +=== Requirements + +Given categories of classes (mixins, values, entities, viewmodels, ...) +the _Metamodel_ is bootstrapped. + +. List of Mixin classes must be final once the Metamodel introspection starts. +For any mixin discovered later, we log a warning. +. The Spec Loader is required to mix in members from above list to all Specs, +except Value-, Abstract-types and Mixins. +. Mixins that apply to java.lang.Object are not mixed in to service-types +. Mixin introspection must not as a side-effect fully introspect other types. +However TYPE_ONLY introspection is allowed. +. The Spec Loader shall support 2 modes of operation + .. EAGER - fully introspect all types, starting with those discovered up front, + then include those discovered during introspection. Then lock down the Metamodel - make it unmodifiable. + .. LAZY - only fully introspect Mixins, introspect the rest on the fly. +. The Spec Loader shall support Specification reloading. (Simply evicting a Spec from the cache might not work, +because we cannot be sure that none of the other Specs still holds a reference to the evicted Spec.) + + +[plantuml,fig-introspection-participants,svg] +.Introspection Participants +---- +@startuml + +object ObjectSpecification { + facets + regular members + mixed-in members +} + +class SpecificationLoader <> { + loadSpecification(Class): ObjectSpecification +} + +hide empty members + +@enduml +---- + + diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderDefault.java index 9ee27844c48..d631c09c355 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderDefault.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderDefault.java @@ -31,22 +31,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Stream; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import jakarta.annotation.Priority; -import jakarta.inject.Inject; -import jakarta.inject.Named; -import jakarta.inject.Provider; - -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.stereotype.Service; - import org.apache.causeway.applib.Identifier; import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.annotation.SemanticsOf; @@ -60,6 +46,7 @@ import org.apache.causeway.commons.internal.base._Lazy; import org.apache.causeway.commons.internal.base._NullSafe; import org.apache.causeway.commons.internal.base._Timing; +import org.apache.causeway.commons.internal.debug._Debug.Profiler; import org.apache.causeway.commons.internal.exceptions._Exceptions; import org.apache.causeway.core.config.CausewayConfiguration; import org.apache.causeway.core.config.beans.CausewayBeanMetaData; @@ -72,21 +59,31 @@ import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel; import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel.PreloadableTypes; import org.apache.causeway.core.metamodel.commons.ClassUtil; -import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.core.metamodel.facetapi.Facet; import org.apache.causeway.core.metamodel.facets.object.grid.GridFacet; +import org.apache.causeway.core.metamodel.object.Mm2YamlUtils; import org.apache.causeway.core.metamodel.progmodel.ProgrammingModel; import org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutor; import org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutor.Substitution; import org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutorRegistry; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; -import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionRequest; +import org.apache.causeway.core.metamodel.spec.impl.IntrospectionStateHandler.IntrospectionRequest; import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure; import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailures; import org.apache.causeway.core.metamodel.valuetypes.ValueSemanticsResolverDefault; import org.apache.causeway.core.security.authorization.manager.ActionSemanticsResolver; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Priority; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; @@ -124,15 +121,15 @@ class SpecificationLoaderDefault private final Provider valueSemanticsResolver; private final ProgrammingModel programmingModel; private PostProcessor postProcessor; + private MixinSpecStreamer mixinSpecStreamer = MixinSpecStreamer.EMPTY; + private final Profiler profiler = Profiler.getInstance(); @Inject public List preloadableTypes = Collections.emptyList(); - @Getter private MetaModelContext metaModelContext; // cannot inject, would cause circular dependency - private FacetProcessor facetProcessor; - private final Map, ObjectSpecificationMutable> cache = new ConcurrentHashMap<>(); + private final Map, ObjectSpecificationBuilder> cache = new ConcurrentHashMap<>(); private final LogicalTypeResolver logicalTypeResolver = new LogicalTypeResolver(); /** @@ -182,7 +179,6 @@ static SpecificationLoaderDefault instanceForTesting( ()->new ValueSemanticsResolverDefault(List.of(), null), classSubstitutorRegistry); - instance.metaModelContext = serviceRegistry.lookupServiceElseFail(MetaModelContext.class); instance.facetProcessor = new FacetProcessor(programmingModel); instance.postProcessor = enablePostprocessors ? new PostProcessor(programmingModel) @@ -198,18 +194,17 @@ public void init() { if (log.isDebugEnabled()) { log.debug("initialising {}", this); } - this.metaModelContext = serviceRegistry.lookupServiceElseFail(MetaModelContext.class); this.facetProcessor = new FacetProcessor(programmingModel); } record SpecCollector( - List knownSpecs, - Map, ObjectSpecificationMutable> valueSpecs, - List domainServiceSpecs, - List mixinSpecs, - List entitySpecs, - List viewmodelSpecs, - List otherSpecs) { + List knownSpecs, + Map, ObjectSpecificationBuilder> valueSpecs, + List domainServiceSpecs, + List mixinSpecs, + List entitySpecs, + List viewmodelSpecs, + List otherSpecs) { SpecCollector() { this(new ArrayList<>(), @@ -218,10 +213,10 @@ record SpecCollector( new ArrayList<>(), new ArrayList<>()); } - public void collect(final @Nullable ObjectSpecificationMutable spec) { + public void collect(final @Nullable ObjectSpecificationBuilder spec) { if(spec==null) return; // might be vetoed knownSpecs.add(spec); - switch (spec.getBeanSort()) { + switch (spec.beanSort()) { case VALUE -> valueSpecs.put(spec.getCorrespondingClass(), spec); case MANAGED_BEAN_CONTRIBUTING -> domainServiceSpecs.add(spec); case MIXIN -> mixinSpecs.add(spec); @@ -233,6 +228,21 @@ public void collect(final @Nullable ObjectSpecificationMutable spec) { } } + @Override + public boolean contains(@Nullable final Class cls) { + return cls!=null + ? cache.containsKey(cls) + : false; + } + + enum Phase { + BEFORE_MIXINS, + DURING_MIXINS, + AFTER_MIXINS, + } + + Phase phase = Phase.BEFORE_MIXINS; + /** * Initializes and wires up, and primes the cache based on any service * classes (provided by the {@link CausewayBeanTypeRegistry}). @@ -250,12 +260,12 @@ public void createMetaModel() { var specs = new SpecCollector(); // preload otherwise not eagerly discovered classes - var prealoadCount = preloadableTypes.stream() + var preloadCount = preloadableTypes.stream() .flatMap(PreloadableTypes::stream) .map(this::loadSpecification) .filter(_NullSafe::isPresent) .count(); - log.info(" - preloaded {} otherwise not eagerly discovered types", prealoadCount); + log.info(" - preloaded {} otherwise not eagerly discovered types", preloadCount); var valueTypesFromProviders = valueSemanticsResolver.get().streamClassesWithValueSemantics() .map(valueClass->CausewayBeanMetaData.value(LogicalType.infer(valueClass), DiscoveredBy.CAUSEWAY_UPFRONT)) @@ -272,21 +282,54 @@ public void createMetaModel() { introspectAndLog("type hierarchies", specs.knownSpecs, IntrospectionRequest.TYPE_ONLY); introspectAndLog("value types", specs.valueSpecs.values(), IntrospectionRequest.FULL); + //this.mixinSpecStreamer = MixinSpecStreamer.EMPTY; + //this.mixinSpecStreamer = new MixinSpecStreamerOnTheFly(this, causewayBeanTypeRegistry); + this.phase = Phase.DURING_MIXINS; introspectAndLog("mixins", specs.mixinSpecs, IntrospectionRequest.FULL); - introspectAndLog("domain services", specs.domainServiceSpecs, IntrospectionRequest.FULL); + // lock down mixins, also assuming none of the previously fully introspected types need any mixins + this.mixinSpecStreamer = new MixinSpecStreamerEager(this, causewayBeanTypeRegistry); + this.phase = Phase.AFTER_MIXINS; + + //TODO expected no entities fully introspected yet. however, some postprocessors, that + // run on mixin-spec have the sideeffect of fully introspecting other types e.g. by asking for the + // members's element type + cache.values().stream() + .filter(spec->!spec.isMixin()) + .filter(spec->!spec.isValue()) + .filter(ObjectSpecificationBuilder::isFullyIntrospected) + .forEach(spec->{ + log.warn("type (non-mixin, non-value) found fully introspected after mixin introspection {}" + + " - reload triggered", spec.getCorrespondingClass()); + //invalidateCache(spec.getCorrespondingClass()); + //reloadSpecification(spec.getCorrespondingClass()); + }); + + introspectAndLog("domain services", specs.domainServiceSpecs, IntrospectionRequest.FULL); //TODO no mixins required either introspectAndLog("entities (%s)".formatted(causewayBeanTypeRegistry.persistenceStack().name()), specs.entitySpecs(), IntrospectionRequest.FULL); introspectAndLog("view models", specs.viewmodelSpecs(), IntrospectionRequest.FULL); serviceRegistry.lookupServiceElseFail(MenuBarsService.class).menuBars(); - if(isFullIntrospect()) { - var snapshot = snapshotSpecifications(); - log.info(" - introspecting all {} types eagerly (FullIntrospect=true)", snapshot.size()); - introspect(snapshot.filter(x->x.getBeanSort().isMixin()), IntrospectionRequest.FULL); - introspect(snapshot.filter(x->!x.getBeanSort().isMixin()), IntrospectionRequest.FULL); + var snapshot = snapshotSpecifications(); + snapshot.stream() + .filter(ObjectSpecificationBuilder::isMixin) + .filter(spec->!spec.isFullyIntrospected()) + .forEach(spec->{ + log.warn("Mixin was missing during first pass {}." + + "It will not be added to the metamodel. For inclusion, " + + "make sure it is discovered by Spring.", spec); + }); + + //if(isFullIntrospect()) //TODO enforced, otherwise types discovered during introspection never get fully introspected (bug) + { + log.info(" - introspecting types not initially discovered by Spring {}", snapshot.size()); + introspect(snapshot.filter(spec->!spec.isMixin()), IntrospectionRequest.FULL); } + //debug + //System.err.println(Mm2YamlUtils.toYaml(snapshotSpecifications())); + log.info(" - running remaining validators"); getOrAssessValidationResult(); // as a side effect memoizes the validation result @@ -302,8 +345,11 @@ public void createMetaModel() { if(isFullIntrospect()) { setMetamodelFullyIntrospected(true); } + + log.info("\n{}", profiler); } + @Override public Optional getValidationResult() { return validationResult.getMemoized(); @@ -394,8 +440,7 @@ public ObjectSpecification loadSpecification( @Override public void validateLater( - final ObjectSpecification objectSpec, - final Supplier introspectionContextProvider) { + final ObjectSpecification objectSpec) { if(!isMetamodelFullyIntrospected()) // don't trigger validation during bootstrapping // getValidationResult() is lazily populated later on first request anyway @@ -405,7 +450,7 @@ public void validateLater( return; if(log.isInfoEnabled()) { - log.info("re-validation triggered by {}", introspectionContextProvider.get()); + log.info("re-validation triggered for {}", objectSpec.getFullIdentifier()); } // validators might discover new specs @@ -436,7 +481,7 @@ public void validateLater( // -- LOOKUP @Override - public Can snapshotSpecifications() { + public Can snapshotSpecifications() { return Can.ofCollection(cache.values()); } @@ -492,11 +537,12 @@ public void addValidationFailure(final ValidationFailure validationFailure) { } } - private _Lazy validationResult = + private final _Lazy validationResult = _Lazy.threadSafe(this::runMetaModelValidators); private final AtomicBoolean validationInProgress = new AtomicBoolean(false); private final BlockingQueue validationQueue = new LinkedBlockingQueue<>(); + //private Can mixinSpecs = Can.empty(); private ValidationFailures runMetaModelValidators() { validationInProgress.set(true); @@ -552,7 +598,7 @@ private CausewayBeanMetaData classify(final @Nullable Class type) { } @Nullable - private ObjectSpecificationMutable primeSpecification( + private ObjectSpecificationBuilder primeSpecification( final @NonNull CausewayBeanMetaData typeMeta) { return loadSpecificationNullable( typeMeta.getCorrespondingClass(), type->typeMeta, IntrospectionRequest.REGISTER); @@ -560,12 +606,13 @@ private ObjectSpecificationMutable primeSpecification( } @Nullable - private ObjectSpecificationMutable loadSpecificationNullable( + private ObjectSpecificationBuilder loadSpecificationNullable( final @Nullable Class type, final @NonNull Function, CausewayBeanMetaData> beanClassifier, final @NonNull IntrospectionRequest request) { - if(type==null) return null; + if(type==null) + return null; var substitute = classSubstitutorRegistry.getSubstitution(type); if (substitute.isNeverIntrospect()) return null; // never inspect @@ -577,9 +624,16 @@ private ObjectSpecificationMutable loadSpecificationNullable( .register( createSpecification(beanClassifier.apply(substitutedType)))); - spec.introspect(request); + if(phase == Phase.DURING_MIXINS + && request==IntrospectionRequest.FULL + && !spec.isMixin()) { + // don't allow the side-effect of fully introspecting other types during mixin introspection + spec.introspect(IntrospectionRequest.TYPE_ONLY); + } else { + spec.introspect(request); + } - if(spec.getAliases().isNotEmpty() + if(spec.aliases().isNotEmpty() // this bool. expr. is an optimization, not strictly required ... a bit of hack though && request == IntrospectionRequest.TYPE_ONLY) { @@ -601,18 +655,19 @@ private ObjectSpecificationMutable loadSpecificationNullable( /** * Creates the appropriate type of {@link ObjectSpecification}. */ - private ObjectSpecificationMutable createSpecification(final CausewayBeanMetaData typeMeta) { + private ObjectSpecificationBuilder createSpecification( + final CausewayBeanMetaData typeMeta) { var objectSpec = new ObjectSpecificationDefault( - typeMeta, - metaModelContext, - facetProcessor, - postProcessor, - classSubstitutorRegistry); + typeMeta, + facetProcessor, + postProcessor, + classSubstitutorRegistry, + ()->mixinSpecStreamer); return objectSpec; } private void introspectSequential( - final Can specs, + final Can specs, final IntrospectionRequest request) { for (var spec : specs) { spec.introspect(request); @@ -620,7 +675,7 @@ private void introspectSequential( } private void introspectParallel( - final Can specs, + final Can specs, final IntrospectionRequest request) { specs.parallelStream() .forEach(spec -> { @@ -635,7 +690,7 @@ private void introspectParallel( private void introspectAndLog( final String info, - final Iterable specs, + final Iterable specs, final IntrospectionRequest request) { var stopWatch = _Timing.now(); introspect(Can.ofIterable(specs), request); @@ -644,7 +699,7 @@ private void introspectAndLog( } private void introspect( - final Can specs, + final Can specs, final IntrospectionRequest request) { if(parallel) { introspectParallel(specs, request); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderInternal.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderInternal.java index 47235a2515e..e1cb8a794c1 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderInternal.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderInternal.java @@ -19,12 +19,6 @@ package org.apache.causeway.core.metamodel.spec.impl; import java.util.Optional; -import java.util.function.Supplier; - -import jakarta.inject.Named; - -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; import org.apache.causeway.applib.id.LogicalType; import org.apache.causeway.applib.services.bookmark.Bookmark; @@ -33,8 +27,12 @@ import org.apache.causeway.commons.internal.exceptions._Exceptions; import org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutor; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; -import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionRequest; +import org.apache.causeway.core.metamodel.spec.impl.IntrospectionStateHandler.IntrospectionRequest; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import jakarta.inject.Named; interface SpecificationLoaderInternal extends SpecificationLoader { @@ -61,9 +59,8 @@ default ObjectSpecification loadSpecification( final @Nullable String logicalTypeName, final @NonNull IntrospectionRequest request) { - if(_Strings.isNullOrEmpty(logicalTypeName)) { - return null; - } + if(_Strings.isNullOrEmpty(logicalTypeName)) + return null; return lookupLogicalType(logicalTypeName) .map(logicalType-> loadSpecification(logicalType.correspondingClass(), request)) @@ -153,15 +150,13 @@ default Optional lookupBeanSort(final @Nullable LogicalType logicalTyp if(logicalType==null) return Optional.empty(); var spec = loadSpecification(logicalType.correspondingClass(), IntrospectionRequest.REGISTER); return spec != null - ? Optional.of(spec.getBeanSort()) + ? Optional.of(spec.beanSort()) : Optional.empty(); } /** * queue {@code objectSpec} for later validation - * @param objectSpec - * @param introspectionContextProvider */ - void validateLater(ObjectSpecification objectSpec, Supplier introspectionContextProvider); + void validateLater(ObjectSpecification objectSpec); } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SyntheticNavigationActionFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SyntheticNavigationActionFactory.java index d03122cfb42..b3d85329415 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SyntheticNavigationActionFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SyntheticNavigationActionFactory.java @@ -19,24 +19,23 @@ package org.apache.causeway.core.metamodel.spec.impl; import java.util.HashSet; +import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; -import org.springframework.util.ClassUtils; - +import org.apache.causeway.applib.annotation.SemanticsOf; import org.apache.causeway.applib.services.command.CommandRecordingSuppressed; import org.apache.causeway.applib.services.metamodel.MetaModelService.AssociationsLookup; import org.apache.causeway.applib.value.Blob; import org.apache.causeway.applib.value.Clob; import org.apache.causeway.commons.collections.Can; -import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.core.metamodel.facetapi.FacetUtil; import org.apache.causeway.core.metamodel.facets.FacetedMethod; +import org.apache.causeway.core.metamodel.facets.actions.semantics.ActionSemanticsFacet; import org.apache.causeway.core.metamodel.facets.actions.synthetic.ActionInvocationFacetForParentedCollectionNavigation; import org.apache.causeway.core.metamodel.facets.actions.synthetic.ActionInvocationFacetForScalarReferenceNavigation; -import org.apache.causeway.applib.annotation.SemanticsOf; -import org.apache.causeway.core.metamodel.facets.actions.semantics.ActionSemanticsFacet; import org.apache.causeway.core.metamodel.facets.actions.synthetic.ActionValidationFacetForParentedCollectionNavigation; import org.apache.causeway.core.metamodel.facets.actions.synthetic.CssClassFacetForParentedCollectionNavigation; import org.apache.causeway.core.metamodel.facets.actions.synthetic.DisabledFacetForEmptyParentedCollectionNavigation; @@ -46,10 +45,11 @@ import org.apache.causeway.core.metamodel.facets.actions.synthetic.LayoutGroupFacetForScalarReferenceNavigation; import org.apache.causeway.core.metamodel.facets.actions.synthetic.LayoutOrderFacetForParentedCollectionNavigation; import org.apache.causeway.core.metamodel.facets.actions.synthetic.ParamNamedFacetForParentedCollectionNavigation; +import org.apache.causeway.core.metamodel.facets.actions.synthetic.ParentedCollectionNavigationFacet; import org.apache.causeway.core.metamodel.facets.actions.synthetic.ParentedCollectionNavigationFacetDefault; +import org.apache.causeway.core.metamodel.facets.actions.synthetic.ScalarReferenceNavigationFacet; import org.apache.causeway.core.metamodel.facets.actions.synthetic.ScalarReferenceNavigationFacetDefault; import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacetForStaticMemberName; -import org.apache.causeway.core.metamodel.facets.members.publish.command.CommandPublishingFacet; import org.apache.causeway.core.metamodel.facets.members.publish.command.CommandPublishingFacetForActionAnnotation; import org.apache.causeway.core.metamodel.facets.object.autocomplete.AutoCompleteFacet; import org.apache.causeway.core.metamodel.facets.objectvalue.choices.ChoicesFacet; @@ -58,13 +58,19 @@ import org.apache.causeway.core.metamodel.facets.properties.choices.PropertyChoicesFacet; import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; -import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer.ColumnQuery; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer.ColumnQuery; import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation; import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; +import org.springframework.util.ClassUtils; -final class SyntheticNavigationActionFactory { +record SyntheticNavigationActionFactory( + ObjectSpecificationBuilder ownerSpec, + List regularAssociations, + List mixedInAssociations, + List regularActions, + List mixedInActions) { static final String ACTION_ID_PREFIX = "__causeway_navigate_to_"; @@ -76,22 +82,32 @@ final class SyntheticNavigationActionFactory { "datanucleusVersionLong", "datanucleusVersionTimestamp"); - private SyntheticNavigationActionFactory() { + List synthesizeNavigationActions() { + var existingActionIds = Stream.concat(regularActions.stream(), mixedInActions.stream()) + .map(ObjectAction::getId) + .collect(Collectors.toSet()); + var existingSyntheticActionIds = Stream.concat(regularActions.stream(), mixedInActions.stream()) + .filter(action -> action.lookupFacet(ParentedCollectionNavigationFacet.class).isPresent() + || action.lookupFacet(ScalarReferenceNavigationFacet.class).isPresent()) + .map(ObjectAction::getId) + .collect(Collectors.toSet()); + var syntheticActions = createFor( + Stream.concat(regularAssociations.stream(), mixedInAssociations.stream()).toList(), + existingActionIds, + existingSyntheticActionIds) + .toList(); + return syntheticActions; } - static Stream createFor( - final MetaModelContext mmc, - final ObjectSpecification ownerSpec, - final Stream associations, + private Stream createFor( + final List candidates, final Set existingActionIds, final Set existingSyntheticActionIds) { if (!(ownerSpec.isEntity() || ownerSpec.isViewModel()) - || CommandRecordingSuppressed.class.isAssignableFrom(ownerSpec.getCorrespondingClass())) { - return Stream.empty(); - } + || CommandRecordingSuppressed.class.isAssignableFrom(ownerSpec.getCorrespondingClass())) + return Stream.empty(); - var candidates = associations.toList(); var generatedIds = new HashSet(); return Stream.concat( @@ -102,7 +118,7 @@ static Stream createFor( .filter(collection -> eligible(ownerSpec, collection)) .filter(collection -> !existingSyntheticActionIds.contains( ACTION_ID_PREFIX + collection.getId())) - .map(collection -> createCollectionAction(mmc, ownerSpec, collection)), + .map(collection -> createCollectionAction(ownerSpec, collection)), candidates.stream() .filter(ObjectAssociation::isOneToOneAssociation) .map(ObjectAssociation::getSpecialization) @@ -110,12 +126,11 @@ static Stream createFor( .filter(reference -> eligible(ownerSpec, reference)) .filter(reference -> !existingSyntheticActionIds.contains( ACTION_ID_PREFIX + reference.getId())) - .map(reference -> createReferenceAction(mmc, ownerSpec, reference))) + .map(reference -> createReferenceAction(ownerSpec, reference))) .peek(action -> { - if (existingActionIds.contains(action.getId()) || !generatedIds.add(action.getId())) { - throw new IllegalStateException("Action id '%s' is reserved for synthetic navigation" + if (existingActionIds.contains(action.getId()) || !generatedIds.add(action.getId())) + throw new IllegalStateException("Action id '%s' is reserved for synthetic navigation" .formatted(action.getId())); - } }); } @@ -136,7 +151,6 @@ private static boolean eligible( } private static ObjectAction createCollectionAction( - final MetaModelContext mmc, final ObjectSpecification ownerSpec, final OneToManyAssociation collection) { @@ -150,14 +164,14 @@ private static ObjectAction createCollectionAction( .map(ObjectAssociation::getId) .toArray(String[]::new); var facetedMethod = FacetedMethod.createSyntheticAction( - mmc, + ownerSpec.getMetaModelContext(), ownerSpec.getCorrespondingClass(), ACTION_ID_PREFIX + collection.getId(), collection.getElementType().getCorrespondingClass(), parameterTypes, parameterNames); - installCommonFacets(mmc, facetedMethod); + installCommonFacets(facetedMethod); FacetUtil.addFacet(new LayoutGroupFacetForParentedCollectionNavigation( collection.getId(), collection.getCanonicalFriendlyName(), facetedMethod)); FacetUtil.addFacet(new LayoutOrderFacetForParentedCollectionNavigation(collection, facetedMethod)); @@ -173,19 +187,18 @@ private static ObjectAction createCollectionAction( } private static ObjectAction createReferenceAction( - final MetaModelContext mmc, final ObjectSpecification ownerSpec, final OneToOneAssociation reference) { var facetedMethod = FacetedMethod.createSyntheticAction( - mmc, + ownerSpec.getMetaModelContext(), ownerSpec.getCorrespondingClass(), ACTION_ID_PREFIX + reference.getId(), reference.getElementType().getCorrespondingClass(), new Class[0], new String[0]); - installCommonFacets(mmc, facetedMethod); + installCommonFacets(facetedMethod); FacetUtil.addFacet(new LayoutGroupFacetForScalarReferenceNavigation( reference.getId(), reference.getCanonicalFriendlyName(), facetedMethod)); FacetUtil.addFacet(new ScalarReferenceNavigationFacetDefault(reference, facetedMethod)); @@ -197,7 +210,6 @@ private static ObjectAction createReferenceAction( } private static void installCommonFacets( - final MetaModelContext mmc, final FacetedMethod facetedMethod) { FacetUtil.addFacet(new MemberNamedFacetForStaticMemberName("Navigate To", facetedMethod)); FacetUtil.addFacet(new CssClassFacetForParentedCollectionNavigation(facetedMethod)); @@ -206,8 +218,8 @@ private static void installCommonFacets( "synthetic navigation", SemanticsOf.SAFE, facetedMethod)); FacetUtil.addFacetIfPresent(CommandPublishingFacetForActionAnnotation.create( Optional.empty(), - mmc.getConfiguration(), - mmc.getServiceInjector(), + facetedMethod.getConfiguration(), + facetedMethod.getServiceInjector(), facetedMethod)); } @@ -219,6 +231,10 @@ private static Can filterPropertiesOf( collection.getFeatureIdentifier(), parentPlaceholder, AssociationsLookup.AVAILABLE); + var elementType = (ObjectSpecificationBuilder)collection.getElementType(); + if(!elementType.isFullyIntrospected()) { + elementType.introspectFully(); + } return collection.getElementType() .streamAssociationsForColumnRendering(columnQuery) .filter(SyntheticNavigationActionFactory::eligibleFilterProperty) @@ -228,9 +244,8 @@ private static Can filterPropertiesOf( private static boolean eligibleFilterProperty(final ObjectAssociation property) { if (!property.isOneToOneAssociation() || EXCLUDED_PARAMETER_PROPERTY_IDS.contains(property.getId()) - || property.getElementType() == null) { - return false; - } + || property.getElementType() == null) + return false; var elementType = property.getElementType(); if (elementType.isValue()) { var type = elementType.getCorrespondingClass(); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ValidatorDomainIncludeAnnotationEnforcesMetamodelContribution.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ValidatorDomainIncludeAnnotationEnforcesMetamodelContribution.java index 6b88f44e868..10aba51d24c 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ValidatorDomainIncludeAnnotationEnforcesMetamodelContribution.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ValidatorDomainIncludeAnnotationEnforcesMetamodelContribution.java @@ -28,8 +28,6 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -import org.jspecify.annotations.NonNull; - import org.apache.causeway.applib.Identifier; import org.apache.causeway.applib.annotation.Domain; import org.apache.causeway.commons.collections.Can; @@ -55,6 +53,7 @@ import org.apache.causeway.core.metamodel.spec.feature.MixedIn; import org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract; import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure; +import org.jspecify.annotations.NonNull; /** * @since 2.0 @@ -68,7 +67,7 @@ class ValidatorDomainIncludeAnnotationEnforcesMetamodelContribution ValidatorDomainIncludeAnnotationEnforcesMetamodelContribution(final MetaModelContext mmc) { super(mmc, spec->((spec instanceof ObjectSpecificationDefault) && !spec.isAbstract() - && !spec.getBeanSort().isManagedBeanNotContributing() + && !spec.beanSort().isManagedBeanNotContributing() && !spec.isValue())); this.classCache = _ClassCache.getInstance(); } @@ -188,7 +187,7 @@ private static void validateOrphanedSupportingMethod( final @NonNull Set alreadyReported) { if(spec.isAbstract() - || spec.getBeanSort().isManagedBeanNotContributing() + || spec.beanSort().isManagedBeanNotContributing() || spec.isValue() || spec.getIntrospectionPolicy() .getSupportMethodAnnotationPolicy() diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MemberIdClashReporting.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MemberIdClashReporting.java index 7d1c6d8068d..66f2470191b 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MemberIdClashReporting.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MemberIdClashReporting.java @@ -19,6 +19,7 @@ package org.apache.causeway.core.metamodel.spec.impl; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -51,8 +52,9 @@ class _MemberIdClashReporting { void flagAnyMemberIdClashes( final ObjectSpecification declaringType, final Iterable regularMembers, - final Iterable mixedInMembers) { + final List mixedInMembers) { + if(mixedInMembers.isEmpty()) return; // nothing to check if(declaringType.isAbstract()) return; // skip abstract types var memberIdCollector = new MemberIdCollector(); @@ -74,11 +76,15 @@ private static class MemberIdCollector { /** Optionally returns a member with the same member-id, based on whether previously collected. */ public Optional collect(final ObjectMember objectMember) { if(objectMember.isAction()) { - if(actionIds==null) this.actionIds = new HashMap<>(); + if(actionIds==null) { + this.actionIds = new HashMap<>(); + } return Optional.ofNullable(actionIds.put(objectMember.getId(), objectMember)); } if(objectMember.isPropertyOrCollection()) { - if(associationIds==null) this.associationIds = new HashMap<>(); + if(associationIds==null) { + this.associationIds = new HashMap<>(); + } return Optional.ofNullable(associationIds.put(objectMember.getId(), objectMember)); } throw _Exceptions.unmatchedCase(String.format("framework bug: unmatched feature %s", objectMember)); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MemberSortingUtils.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MemberSortingUtils.java index 1a3f41022d7..07e6eb06c65 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MemberSortingUtils.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MemberSortingUtils.java @@ -18,12 +18,15 @@ */ package org.apache.causeway.core.metamodel.spec.impl; +import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; import org.apache.causeway.applib.exceptions.unrecoverable.UnknownTypeException; import org.apache.causeway.commons.internal.collections._Lists; +import org.apache.causeway.commons.internal.collections._Streams; import org.apache.causeway.core.metamodel.layout.DeweyOrderSet; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation; @@ -32,56 +35,73 @@ /** package private utility */ final class _MemberSortingUtils { - // -- ASSOCIATION SORTING + // -- ASSOCIATION SORTING - static List sortAssociationsIntoList(final Stream associations) { - var deweyOrderSet = DeweyOrderSet.createOrderSet(associations); - var orderedAssociations = _Lists. newArrayList(); - sortAssociations(deweyOrderSet, orderedAssociations); - return orderedAssociations; + static List associationsInOrder( + final ObjectSpecification objSpec, + final List regularAssociations, + final List mixedInAssociations) { + _MemberIdClashReporting.flagAnyMemberIdClashes(objSpec, regularAssociations, mixedInAssociations); // do before sorting + return sortAssociationsIntoList(Stream.concat( + regularAssociations.stream(), + mixedInAssociations.stream())); } // -- ACTION SORTING - static List sortActionsIntoList(final Stream actions) { - var deweyOrderSet = DeweyOrderSet.createOrderSet(actions); - var orderedActions = _Lists.newArrayList(); - sortActions(deweyOrderSet, orderedActions); - return orderedActions; + static List actionsInOrder( + final ObjectSpecification objSpec, + final List regularActions, + final List mixedInActions, + final List syntheticActions) { + _MemberIdClashReporting.flagAnyMemberIdClashes(objSpec, regularActions, mixedInActions); // do before sorting + return sortActionsIntoList(_Streams.concat( + regularActions.stream(), + mixedInActions.stream(), + syntheticActions.stream())); } // -- HELPER + private static List sortAssociationsIntoList(final Stream associations) { + var deweyOrderSet = DeweyOrderSet.createOrderSet(associations); + var orderedAssociations = new ArrayList(); + sortAssociations(deweyOrderSet, orderedAssociations); + return orderedAssociations; + } + + private static List sortActionsIntoList(final Stream actions) { + var deweyOrderSet = DeweyOrderSet.createOrderSet(actions); + var orderedActions = new ArrayList(); + sortActions(deweyOrderSet, orderedActions); + return orderedActions; + } + private static void sortAssociations(final DeweyOrderSet orderSet, final List associationsToAppendTo) { for (final Object element : orderSet) { if (element instanceof OneToManyAssociation) { associationsToAppendTo.add((ObjectAssociation) element); } else if (element instanceof OneToOneAssociation) { associationsToAppendTo.add((ObjectAssociation) element); - } else if (element instanceof DeweyOrderSet) { + } else if (element instanceof DeweyOrderSet childOrderSet) { // just flatten. - DeweyOrderSet childOrderSet = (DeweyOrderSet) element; sortAssociations(childOrderSet, associationsToAppendTo); - } else { - throw new UnknownTypeException(element); - } + } else + throw new UnknownTypeException(element); } } private static void sortActions(final DeweyOrderSet orderSet, final List actionsToAppendTo) { for (var element : orderSet) { - if(element instanceof ObjectAction) { - var objectAction = (ObjectAction) element; + if(element instanceof ObjectAction objectAction) { actionsToAppendTo.add(objectAction); } - else if (element instanceof DeweyOrderSet) { - var deweyOrderSet = ((DeweyOrderSet) element); + else if (element instanceof DeweyOrderSet deweyOrderSet) { var actions = _Lists.newArrayList(); sortActions(deweyOrderSet, actions); actionsToAppendTo.addAll(actions); - } else { - throw new UnknownTypeException(element); - } + } else + throw new UnknownTypeException(element); } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MembersAsColumns.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MembersAsColumns.java index f5c3ddaff9b..81c489f36ee 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MembersAsColumns.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MembersAsColumns.java @@ -29,9 +29,6 @@ import java.util.Optional; import java.util.stream.Stream; -import org.jspecify.annotations.NonNull; - -import org.apache.causeway.applib.annotation.Where; import org.apache.causeway.applib.layout.component.PropertyLayoutData; import org.apache.causeway.applib.services.tablecol.TableColumnOrderService; import org.apache.causeway.applib.services.tablecol.TableColumnVisibilityService; @@ -42,9 +39,9 @@ import org.apache.causeway.core.metamodel.facets.object.grid.GridFacet; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.MixedIn; -import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer.ColumnQuery; +import org.jspecify.annotations.NonNull; record _MembersAsColumns( boolean isColumnOrderPatchingEnabled, @@ -58,16 +55,17 @@ record _MembersAsColumns( mmc.getServiceRegistry().select(TableColumnOrderService.class)); } - public Stream streamActionsForColumnRendering( - final ObjectSpecification elementType, - final Where where) { - if(elementType.isValue()) - return Stream.empty(); - - return elementType.streamRuntimeActions(MixedIn.INCLUDED) - .filter(ObjectAction.Predicates.visibleAccordingToHiddenFacet(where)) - .sorted((a, b)->a.getCanonicalFriendlyName().compareTo(b.getCanonicalFriendlyName())); - } +//inlined to ActionContainer +// public Stream streamActionsForColumnRendering( +// final ObjectSpecification elementType, +// final Where where) { +// if(elementType.isValue()) +// return Stream.empty(); +// +// return elementType.streamRuntimeActions(MixedIn.INCLUDED) +// .filter(ObjectAction.Predicates.visibleAccordingToHiddenFacet(where)) +// .sorted((a, b)->a.getCanonicalFriendlyName().compareTo(b.getCanonicalFriendlyName())); +// } /** * @param parentObject not used for standalone tables and allowed to be empty for parented ones diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MixedInMemberFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MixedInMemberFactory.java deleted file mode 100644 index b895c9d5fbf..00000000000 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MixedInMemberFactory.java +++ /dev/null @@ -1,56 +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. - */ -package org.apache.causeway.core.metamodel.spec.impl; - -import java.util.function.Function; - -import org.apache.causeway.core.metamodel.spec.ObjectSpecification; -import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; - -import lombok.experimental.UtilityClass; - -/** package private utility */ -@UtilityClass -class _MixedInMemberFactory { - - // -- MIXINS - - Function mixedInAction( - final ObjectSpecification mixeeSpec, - final ObjectSpecification mixinSpec, - final String mixinMethodName) { - - return mixinAction -> new ObjectActionMixedIn( - mixinSpec, mixinMethodName, mixinAction, mixeeSpec); - } - - Function mixedInAssociation( - final ObjectSpecification mixeeSpec, - final ObjectSpecification mixinSpec, - final String mixinMethodName) { - - return mixinAction -> - mixinAction.getReturnType().isSingular() - ? new OneToOneAssociationMixedIn( - mixeeSpec, mixinAction, mixinSpec, mixinMethodName) - : new OneToManyAssociationMixedIn( - mixeeSpec, mixinAction, mixinSpec, mixinMethodName); - } - -} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_ValidateUtil.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_ValidateUtil.java index 31034aafb75..96b56a1bcd3 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_ValidateUtil.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_ValidateUtil.java @@ -44,18 +44,18 @@ void runValidators( var snapshot = specLoader.snapshotSpecifications(); programmingModel.streamValidators() - .filter(MetaModelValidator::isEnabled) - .forEach(validator -> { - log.debug("Running validator: {}", validator); - try { - runValidator(validator, snapshot); - } catch (Throwable t) { - log.error("failure", t); - throw t; - } finally { - log.debug("Done validator: {}", validator); - } - }); + .filter(MetaModelValidator::isEnabled) + .forEach(validator -> { + log.debug("Running validator: {}", validator); + try { + runValidator(validator, snapshot); + } catch (Throwable t) { + log.error("failure", t); + throw t; + } finally { + log.debug("Done validator: {}", validator); + } + }); log.debug("Done running MetaModelValidators."); } @@ -89,7 +89,7 @@ private void runValidator( objValidator.validateObjectEnter(objSpec); actionValidator - .ifPresentOrElse( + .ifPresentOrElse( validator-> objSpec.streamRuntimeActions(MixedIn.INCLUDED) .forEach(act->{ @@ -107,16 +107,16 @@ private void runValidator( ); propertyValidator - .ifPresent(validator->{ - objSpec.streamProperties(MixedIn.INCLUDED) - .forEach(prop->validator.validateProperty(objSpec, prop)); - }); + .ifPresent(validator->{ + objSpec.streamProperties(MixedIn.INCLUDED) + .forEach(prop->validator.validateProperty(objSpec, prop)); + }); collectionValidator - .ifPresent(validator->{ - objSpec.streamCollections(MixedIn.INCLUDED) - .forEach(coll->validator.validateCollection(objSpec, coll)); - }); + .ifPresent(validator->{ + objSpec.streamCollections(MixedIn.INCLUDED) + .forEach(coll->validator.validateCollection(objSpec, coll)); + }); objValidator.validateObjectExit(objSpec); } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoader.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoader.java index 6836a9bb6fa..9f1945df713 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoader.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoader.java @@ -21,9 +21,6 @@ import java.util.Optional; import java.util.function.Consumer; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - import org.apache.causeway.applib.Identifier; import org.apache.causeway.applib.id.LogicalType; import org.apache.causeway.applib.services.bookmark.Bookmark; @@ -38,6 +35,8 @@ import org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidator; import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure; import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailures; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** * Builds the meta-model, utilizing an instance of {@link ProgrammingModel} @@ -195,4 +194,6 @@ default ObjectFeature loadFeatureElseFail(final @NonNull Identifier featureIdent //TODO[causeway-core-metamodel-CAUSEWAY-3834] remove from this interface @Nullable ObjectSpecification loadSpecification(@Nullable Class domainType); + boolean contains(@Nullable Class cls); + } diff --git a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/object/mixin/MixinFacetAbstract_Test.java b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/object/mixin/MixinFacetAbstract_Test.java index 9604bc357f2..7dc7495b6b3 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/object/mixin/MixinFacetAbstract_Test.java +++ b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/object/mixin/MixinFacetAbstract_Test.java @@ -53,4 +53,26 @@ void happy_case() throws Exception { Assertions.assertThat(candidate).isTrue(); } + public record MixinAsRecord( + SimpleObject mixee) { + public int prop() { return 0; } + } + + @Test + void mixinAsRecord() throws Exception { + // given + var constructor = MixinAsRecord.class.getConstructor(SimpleObject.class); + var facet = new MixinFacetAbstract( + MixinAsRecord.class, "prop", constructor, null) {}; + + var propMethod = _GenericResolver.testing + .resolveMethod(MixinAsRecord.class, "prop"); + + // when + var candidate = facet.isCandidateForMain(propMethod); + + // then + Assertions.assertThat(candidate).isTrue(); + } + } \ No newline at end of file diff --git a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionState_comparable_Test.java b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionState_comparable_Test.java index 2f2237bfa34..c21c7af8226 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionState_comparable_Test.java +++ b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionState_comparable_Test.java @@ -18,16 +18,15 @@ */ package org.apache.causeway.core.metamodel.spec.impl; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.apache.causeway.core.metamodel.spec.impl.IntrospectionStateHandler.IntrospectionState; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationDefault.IntrospectionState; - public class IntrospectionState_comparable_Test { @Test diff --git a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/ObjectActionParameterAbstractTest_getId_and_getName.java b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/ObjectActionParameterAbstractTest_getId_and_getName.java index 69aad77308a..694f2ab3e84 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/ObjectActionParameterAbstractTest_getId_and_getName.java +++ b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/ObjectActionParameterAbstractTest_getId_and_getName.java @@ -18,10 +18,6 @@ */ package org.apache.causeway.core.metamodel.spec.impl; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -30,7 +26,11 @@ import org.apache.causeway.applib.annotation.Nature; import org.apache.causeway.core.metamodel.execution.MemberExecutorService; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.metamodel.spec.impl.IntrospectionStateHandler.IntrospectionRequest; import org.apache.causeway.core.mmtestsupport.MetaModelContext_forTesting; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; class ObjectActionParameterAbstractTest_getId_and_getName { @@ -47,7 +47,7 @@ public void setUp() { var mmc = MetaModelContext_forTesting.builder() .memberExecutor(Mockito.mock(MemberExecutorService.class)) .build(); - var spec = mmc.getSpecificationLoader().loadSpecification(Customer.class); + var spec = ((SpecificationLoaderInternal)mmc.getSpecificationLoader()).loadSpecification(Customer.class, IntrospectionRequest.FULL); action = spec.getAction("aMethod").orElseThrow(); } diff --git a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/SyntheticNavigationActionTest.java b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/SyntheticNavigationActionTest.java index 5545c142b04..18cfbaaf869 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/SyntheticNavigationActionTest.java +++ b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/SyntheticNavigationActionTest.java @@ -18,18 +18,13 @@ */ package org.apache.causeway.core.metamodel.spec.impl; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import java.util.ArrayList; import java.util.List; import java.util.Map; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import org.springframework.boot.test.util.TestPropertyValues; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - import org.apache.causeway.applib.annotation.Bounding; import org.apache.causeway.applib.annotation.CollectionLayout; import org.apache.causeway.applib.annotation.DomainObject; @@ -63,7 +58,12 @@ import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.MixedIn; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter; import org.apache.causeway.core.mmtestsupport.MetaModelContext_forTesting; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.boot.test.util.TestPropertyValues; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -228,7 +228,7 @@ public Lease __causeway_navigate_to_reference() { @Test void registered_postprocessor_synthesizes_actions_before_later_action_processing() { var mmc = context(true, true); - var spec = mmc.getSpecificationLoader().loadSpecification(Lease.class); + var spec = mmc.getSpecificationLoader().specForTypeElseFail(Lease.class); assertThat(spec.getAction("__causeway_navigate_to_items")).isPresent(); } @@ -251,13 +251,8 @@ void synthesis_is_configuration_gated_and_suppressed_by_owner_marker() { @Test void repeated_synthesis_is_an_idempotent_no_op() { var mmc = context(true); - var spec = mmc.getSpecificationLoader().loadSpecification(Lease.class); - spec.streamDeclaredAssociations(MixedIn.INCLUDED).toList(); - var mutableSpec = (ObjectSpecificationMutable) spec; - - mutableSpec.synthesizeNavigationActions(); - mutableSpec.synthesizeNavigationActions(); - + var spec = mmc.getSpecificationLoader().specForTypeElseFail(Lease.class); + Assertions.assertTrue(((ObjectSpecificationBuilder)spec).isFullyIntrospected()); assertThat(spec.streamRuntimeActions(MixedIn.INCLUDED) .filter(action -> action.getId().equals("__causeway_navigate_to_items")) .count()).isEqualTo(1L); @@ -285,7 +280,7 @@ void collection_action_has_stable_safe_framework_metadata_and_normal_publication void collection_parameters_follow_columns_and_exclude_hidden_large_and_unconstrained_properties() { var action = action(context(true), Lease.class, "items").orElseThrow(); - assertThat(action.getParameters().stream().map(parameter -> parameter.getId()).toList()) + assertThat(action.getParameters().stream().map(ObjectActionParameter::getId).toList()) .containsExactly( "name", "checkbox", @@ -294,7 +289,7 @@ void collection_parameters_follow_columns_and_exclude_hidden_large_and_unconstra "boundedReference", "autocompleteReference", "objectAutocompleteReference"); - assertThat(action.getParameters().stream().allMatch(parameter -> parameter.isOptional())).isTrue(); + assertThat(action.getParameters().stream().allMatch(ObjectActionParameter::isOptional)).isTrue(); assertThat(action.getParameters().stream() .filter(parameter -> parameter.getId().equals("checkbox")) .findFirst().orElseThrow().getElementType().getCorrespondingClass()) @@ -428,9 +423,8 @@ private static java.util.Optional action( final MetaModelContext_forTesting mmc, final Class ownerType, final String associationId) { - ObjectSpecification spec = mmc.getSpecificationLoader().loadSpecification(ownerType); - spec.streamDeclaredAssociations(MixedIn.INCLUDED).toList(); - ((ObjectSpecificationMutable) spec).synthesizeNavigationActions(); + ObjectSpecification spec = mmc.getSpecificationLoader().specForTypeElseFail(ownerType); + Assertions.assertTrue(((ObjectSpecificationBuilder)spec).isFullyIntrospected()); return spec.getAction(SyntheticNavigationActionFactory.ACTION_ID_PREFIX + associationId); } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/factory/FactoryServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/factory/FactoryServiceDefault.java index eb6cf27282a..9a680c47dc2 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/factory/FactoryServiceDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/factory/FactoryServiceDefault.java @@ -20,14 +20,6 @@ import java.util.Optional; -import jakarta.annotation.Priority; -import jakarta.inject.Named; -import jakarta.inject.Provider; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.jspecify.annotations.Nullable; -import org.springframework.stereotype.Service; - import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.graph.tree.TreeNode; import org.apache.causeway.applib.services.bookmark.Bookmark; @@ -43,8 +35,14 @@ import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices; - import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Priority; +import jakarta.inject.Named; +import jakarta.inject.Provider; /** * Default implementation of {@link FactoryService}. @@ -67,9 +65,8 @@ public record FactoryServiceDefault( @Override public T getOrCreate(final @NonNull Class requiredType) { var spec = loadSpecElseFail(requiredType); - if(spec.isInjectable()) { - return get(requiredType); - } + if(spec.isInjectable()) + return get(requiredType); return create(requiredType); } @@ -83,9 +80,8 @@ public T get(final @NonNull Class requiredType) { @Override public T detachedEntity(final @NonNull Class domainClass) { var entitySpec = loadSpecElseFail(domainClass); - if(!entitySpec.isEntity()) { - throw _Exceptions.illegalArgument("Class '%s' is not an entity", domainClass.getName()); - } + if(!entitySpec.isEntity()) + throw _Exceptions.illegalArgument("Class '%s' is not an entity", domainClass.getName()); return createObject(domainClass, entitySpec); } @@ -93,26 +89,28 @@ public T detachedEntity(final @NonNull Class domainClass) { public T detachedEntity(final @NonNull T entityPojo) { var entityClass = entityPojo.getClass(); var spec = loadSpecElseFail(entityClass); - if(!spec.isEntity()) { - throw _Exceptions.illegalArgument("Type '%s' is not recognized as an entity type by the framework.", + if(!spec.isEntity()) + throw _Exceptions.illegalArgument("Type '%s' is not recognized as an entity type by the framework.", entityClass); - } objectLifecyclePublisher().onPostCreate(ManagedObject.entity(spec, entityPojo, Optional.empty())); return entityPojo; } @Override public T mixin(final @NonNull Class mixinClass, final @NonNull Object mixee) { + if(!specificationLoaderProvider().get().contains(mixinClass)) + throw _Exceptions.illegalArgument("Mixin class '%s' is not part of the meta model, hence will not be loaded, " + + "because that would invalidate the entire metamodel as currently held in memory", + mixinClass.getName()); + var mixinSpec = loadSpecElseFail(mixinClass); - var mixinFacet = mixinSpec.getFacet(MixinFacet.class); - if(mixinFacet == null) { - throw _Exceptions.illegalArgument("Class '%s' is not a mixin", + var mixinFacet = mixinSpec.lookupFacet(MixinFacet.class).orElse(null); + if(mixinFacet == null) + throw _Exceptions.illegalArgument("Class '%s' is not a mixin", mixinClass.getName()); - } - if(mixinSpec.isAbstract()) { - throw _Exceptions.illegalArgument("Cannot instantiate abstract type '%s' as a mixin", + if(mixinSpec.isAbstract()) + throw _Exceptions.illegalArgument("Cannot instantiate abstract type '%s' as a mixin", mixinClass.getName()); - } var mixin = mixinFacet.instantiate(mixee); return _Casts.uncheckedCast(mixin); } @@ -121,10 +119,9 @@ public T mixin(final @NonNull Class mixinClass, final @NonNull Object mix public T viewModel(final @NonNull T viewModelPojo) { var viewModelClass = viewModelPojo.getClass(); var spec = loadSpecElseFail(viewModelClass); - if(!spec.isViewModel()) { - throw _Exceptions.illegalArgument("Type '%s' is not recognized as a ViewModel by the framework.", + if(!spec.isViewModel()) + throw _Exceptions.illegalArgument("Type '%s' is not recognized as a ViewModel by the framework.", viewModelClass); - } spec.viewmodelFacetElseFail().initialize(viewModelPojo); objectLifecyclePublisher().onPostCreate(ManagedObject.viewmodel(spec, viewModelPojo, Optional.empty())); return viewModelPojo; @@ -139,16 +136,13 @@ public T viewModel(final @NonNull Class viewModelClass, final @Nullable B @Override public T create(final @NonNull Class domainClass) { var spec = loadSpecElseFail(domainClass); - if(spec.isInjectable()) { - throw _Exceptions.illegalArgument( + if(spec.isInjectable()) + throw _Exceptions.illegalArgument( "Class '%s' is managed by Spring, use get() instead", domainClass.getName()); - } - if(spec.isViewModel()) { - return createViewModelElseFail(domainClass, spec, Optional.empty()); - } - if(spec.isEntity()) { - return detachedEntity(domainClass); - } + if(spec.isViewModel()) + return createViewModelElseFail(domainClass, spec, Optional.empty()); + if(spec.isEntity()) + return detachedEntity(domainClass); // fallback to generic object creation return createObject(domainClass, spec); } @@ -184,7 +178,7 @@ private T createObject( } @Override - public TreeNode treeNode(T root) { + public TreeNode treeNode(final T root) { return TreeNode.root(root, _Casts.uncheckedCast(new ObjectTreeAdapter(specificationLoaderProvider().get()))); } diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java index a62f77cd977..0145785f55c 100644 --- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java @@ -29,20 +29,6 @@ import java.util.concurrent.Executors; import java.util.function.BiConsumer; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import jakarta.annotation.Priority; -import jakarta.inject.Inject; -import jakarta.inject.Named; -import jakarta.inject.Provider; - -import org.jspecify.annotations.NonNull; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; - import org.apache.causeway.applib.annotation.PriorityPrecedence; import org.apache.causeway.applib.services.factory.FactoryService; import org.apache.causeway.applib.services.iactn.InteractionContext; @@ -83,7 +69,18 @@ import org.apache.causeway.core.runtimeservices.wrapper.dispatchers.InteractionEventDispatcherTypeSafe; import org.apache.causeway.core.runtimeservices.wrapper.handlers.ProxyGenerator; import org.apache.causeway.core.runtimeservices.wrapper.internal.CommandRecord; +import org.jspecify.annotations.NonNull; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Priority; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; import lombok.Getter; import lombok.experimental.Accessors; @@ -212,7 +209,7 @@ public T unwrap(final T t) { // -- ASYNC WRAPPING - AsyncExecutor asyncExecutor(AsyncControl asyncControl) { + AsyncExecutor asyncExecutor(final AsyncControl asyncControl) { return new AsyncExecutor( interactionServiceProvider.get(), transactionServiceProvider.get(), @@ -224,7 +221,7 @@ AsyncExecutor asyncExecutor(AsyncControl asyncControl) { } @Override - public AsyncProxy asyncWrap(T domainObject, AsyncControl asyncControl) { + public AsyncProxy asyncWrap(final T domainObject, final AsyncControl asyncControl) { var pojo = unwrap(domainObject); var proxy = wrap(pojo, asyncControl.syncControl()); return new AsyncProxyInternal<>( @@ -278,19 +275,17 @@ private ManagedObject adaptAndGuardAgainstWrappingNotSupported( var adapter = getObjectManager().adapt(domainObject); if(ManagedObjects.isNullOrUnspecifiedOrEmpty(adapter) - || !adapter.objSpec().getBeanSort().policy().isWrappingSupported()) { - throw _Exceptions.illegalArgument("Cannot wrap an object of type %s", + || !adapter.objSpec().beanSort().policy().isWrappingSupported()) + throw _Exceptions.illegalArgument("Cannot wrap an object of type %s", domainObject.getClass().getName()); - } return adapter; } - private void guardAgainstMixin(ManagedObject mo) { - if(mo.objSpec().isMixin()) { - throw _Exceptions.illegalArgument("cannot wrap a mixin instance directly, " + private void guardAgainstMixin(final ManagedObject mo) { + if(mo.objSpec().isMixin()) + throw _Exceptions.illegalArgument("cannot wrap a mixin instance directly, " + "use WrapperFactory.wrapMixin(...) instead"); - } } // -- HELPER - SETUP diff --git a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/RuntimeServicesTestAbstract.java b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/RuntimeServicesTestAbstract.java index 7cbd70ed574..461bfa46750 100644 --- a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/RuntimeServicesTestAbstract.java +++ b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/RuntimeServicesTestAbstract.java @@ -82,11 +82,9 @@ final void setUp() throws Exception { mmcBuilder.singletonProvider( SingletonBeanProvider - .forTestingLazy(MenuBarsLoaderService.class, ()->{ - return new MenuBarsLoaderServiceDefault( - menubarsLayoutXmlResourceRef, - CommonMimeType.XML); // format under test - })); + .forTestingLazy(MenuBarsLoaderService.class, () -> new MenuBarsLoaderServiceDefault( + menubarsLayoutXmlResourceRef, + CommonMimeType.XML))); mmcBuilder.singletonProvider( SingletonBeanProvider @@ -112,7 +110,6 @@ final void setUp() throws Exception { @AfterEach final void tearDown() throws Exception { onTearDown(); - metaModelContext.getSpecificationLoader().disposeMetaModel(); metaModelContext = null; } diff --git a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorInteractionAdvisorTest.java b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorInteractionAdvisorTest.java index 57bf8111c70..6a5c8a7eb5f 100644 --- a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorInteractionAdvisorTest.java +++ b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorInteractionAdvisorTest.java @@ -18,10 +18,15 @@ */ package org.apache.causeway.core.runtimeservices.command; -import java.util.Optional; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; -import org.junit.jupiter.api.Test; -import org.mockito.InOrder; +import java.util.Optional; import org.apache.causeway.applib.annotation.Where; import org.apache.causeway.applib.services.metamodel.BeanSort; @@ -40,14 +45,8 @@ import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; class CommandExecutorInteractionAdvisorTest { @@ -281,7 +280,7 @@ private static ManagedObject managedObject(final Object pojo) { var specification = mock(ObjectSpecification.class); var specificationLoader = mock(SpecificationLoader.class); when(specification.isValue()).thenReturn(true); - when(specification.getBeanSort()).thenReturn(BeanSort.VALUE); + when(specification.beanSort()).thenReturn(BeanSort.VALUE); when(specification.getSpecificationLoader()).thenReturn(specificationLoader); when(specificationLoader.specForType(pojo.getClass())).thenReturn(Optional.of(specification)); return ManagedObject.value(specification, pojo); diff --git a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefaultTest.java b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefaultTest.java index e1550f06cf8..c727fd57b42 100644 --- a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefaultTest.java +++ b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefaultTest.java @@ -18,13 +18,15 @@ */ package org.apache.causeway.core.runtimeservices.executor; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + import java.util.Optional; import java.util.UUID; -import jakarta.inject.Provider; - -import org.junit.jupiter.api.Test; - import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.applib.services.command.Command; import org.apache.causeway.applib.services.command.CommandRecordingSuppressed; @@ -44,12 +46,9 @@ import org.apache.causeway.core.metamodel.spec.feature.ObjectMember; import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; +import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; +import jakarta.inject.Provider; class MemberExecutorServiceDefaultTest { @@ -219,7 +218,7 @@ private ManagedObject managedObject(final Object pojo) { var objectSpecification = mock(ObjectSpecification.class); var specificationLoader = mock(SpecificationLoader.class); when(objectSpecification.isValue()).thenReturn(true); - when(objectSpecification.getBeanSort()).thenReturn(BeanSort.VALUE); + when(objectSpecification.beanSort()).thenReturn(BeanSort.VALUE); when(objectSpecification.getSpecificationLoader()).thenReturn(specificationLoader); when(specificationLoader.specForType(pojo.getClass())).thenReturn(Optional.of(objectSpecification)); return ManagedObject.value(objectSpecification, pojo); diff --git a/extensions/core/excel/testing/src/main/java/org/apache/causeway/extensions/excel/testing/ExcelFixture.java b/extensions/core/excel/testing/src/main/java/org/apache/causeway/extensions/excel/testing/ExcelFixture.java index 25315d8468d..3f7b870e1c0 100644 --- a/extensions/core/excel/testing/src/main/java/org/apache/causeway/extensions/excel/testing/ExcelFixture.java +++ b/extensions/core/excel/testing/src/main/java/org/apache/causeway/extensions/excel/testing/ExcelFixture.java @@ -26,9 +26,6 @@ import java.util.Map; import java.util.Optional; -import jakarta.inject.Inject; -import jakarta.inject.Named; - import org.apache.causeway.applib.annotation.DomainObject; import org.apache.causeway.applib.annotation.Programmatic; import org.apache.causeway.applib.annotation.PropertyLayout; @@ -48,6 +45,8 @@ import org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScriptWithExecutionStrategy; import org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScripts; +import jakarta.inject.Inject; +import jakarta.inject.Named; import lombok.Getter; import lombok.Setter; @@ -91,14 +90,13 @@ private ExcelFixture(final List> classes) { var beanSort = Optional.ofNullable(specLoader) .flatMap(sl->sl.specForType(cls)) .filter(_NullSafe::isPresent) - .map(ObjectSpecification::getBeanSort) + .map(ObjectSpecification::beanSort) .orElse(BeanSort.UNKNOWN); - if (!beanSort.isViewModel() && !beanSort.isEntity()) { - throw new IllegalArgumentException(String.format( + if (!beanSort.isViewModel() && !beanSort.isEntity()) + throw new IllegalArgumentException(String.format( "Class '%s' does not implement '%s', nor is it persistable", cls.getSimpleName(), ExcelFixtureRowHandler.class.getSimpleName())); - } } this.classes = classes; } @@ -169,8 +167,7 @@ private List create( final Object rowObj, final ExecutionContext ec, final Object previousRow) { - if (rowObj instanceof ExcelFixtureRowHandler) { - final ExcelFixtureRowHandler rowHandler = (ExcelFixtureRowHandler) rowObj; + if (rowObj instanceof final ExcelFixtureRowHandler rowHandler) { return rowHandler.handleRow(ec, this, previousRow); } else { repositoryService.persist(rowObj); diff --git a/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomainWithPdfJsViewer_IntegTest.dump_facets.approved.xml b/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomainWithPdfJsViewer_IntegTest.dump_facets.approved.xml index 5e970ad06d7..1e966eddca8 100644 --- a/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomainWithPdfJsViewer_IntegTest.dump_facets.approved.xml +++ b/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomainWithPdfJsViewer_IntegTest.dump_facets.approved.xml @@ -1581,8 +1581,8 @@ - + diff --git a/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomain_IntegTest.dump_facets.approved.xml b/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomain_IntegTest.dump_facets.approved.xml index 9bc33507ba9..037db5d6090 100644 --- a/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomain_IntegTest.dump_facets.approved.xml +++ b/extensions/vw/pdfjs/metamodel/src/test/java/org/apache/causeway/extensions/pdfjs/metamodel/PdfjsViewer_MixinDomain_IntegTest.dump_facets.approved.xml @@ -1574,8 +1574,8 @@ - + diff --git a/parent/pom.xml b/parent/pom.xml index 46e832a2595..30bb1d41783 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -399,6 +399,8 @@ @DisabledIfSystemProperty(named = "isRunningWithSurefire", matches = "true") --> true + + WARN diff --git a/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/conf/Configuration_usingJpa.java b/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/conf/Configuration_usingJpa.java index 8c722a36151..4a8e20b34dd 100644 --- a/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/conf/Configuration_usingJpa.java +++ b/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/conf/Configuration_usingJpa.java @@ -18,12 +18,6 @@ */ package org.apache.causeway.testdomain.jpa.conf; -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.PropertySource; -import org.springframework.context.annotation.PropertySources; - import org.apache.causeway.core.config.presets.CausewayPresets; import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices; import org.apache.causeway.persistence.jpa.eclipselink.CausewayModulePersistenceJpaEclipselink; @@ -33,6 +27,11 @@ import org.apache.causeway.testdomain.model.stereotypes.MyService; import org.apache.causeway.testdomain.util.kv.KVStoreForTesting; import org.apache.causeway.testing.fixtures.applib.CausewayModuleTestingFixturesApplib; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.PropertySource; +import org.springframework.context.annotation.PropertySources; @SpringBootConfiguration @EnableAutoConfiguration @@ -44,7 +43,7 @@ CausewayModuleTestingFixturesApplib.class, KVStoreForTesting.class, // Helper for JUnit Tests JpaRestEndpointService.class, - + JpaTestDomainModule.class, }) @PropertySources({ diff --git a/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/entities/JpaBook_delete.java b/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/entities/JpaBook_delete.java index 9faec76e9bc..10e3a4b9725 100644 --- a/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/entities/JpaBook_delete.java +++ b/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/entities/JpaBook_delete.java @@ -43,10 +43,10 @@ public class JpaBook_delete { @Inject private RepositoryService repository; - private final JpaBook holder; + private final JpaBook mixee; @MemberSupport public Collection act() { - repository.remove(holder); + repository.remove(mixee); return repository.allInstances(JpaBook.class); } diff --git a/regressiontests/base/src/main/java/org/apache/causeway/testdomain/model/good/ProperMixinContribution_actionRecord.java b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/model/good/ProperMixinContribution_actionRecord.java new file mode 100644 index 00000000000..1b034805969 --- /dev/null +++ b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/model/good/ProperMixinContribution_actionRecord.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.testdomain.model.good; + +import org.apache.causeway.applib.value.Blob; +import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType; + +/** + * For (test) mixin descriptions see {@link ProperMixinContribution}. + */ +//TODO WIP //@Action +public record ProperMixinContribution_actionRecord( + ProperMixinContribution mixee) { + + //@Action(semantics = SemanticsOf.SAFE) + private Blob act() { + return Blob.of("sample", CommonMimeType.BIN, null); + } + +} diff --git a/regressiontests/base/src/main/java/org/apache/causeway/testdomain/model/valuetypes/composite/CalendarEventJaxbVm.java b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/model/valuetypes/composite/CalendarEventJaxbVm.java index bc6e46d9323..b123edb4c24 100644 --- a/regressiontests/base/src/main/java/org/apache/causeway/testdomain/model/valuetypes/composite/CalendarEventJaxbVm.java +++ b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/model/valuetypes/composite/CalendarEventJaxbVm.java @@ -20,17 +20,12 @@ import java.util.List; -import jakarta.inject.Named; -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlRootElement; -import jakarta.xml.bind.annotation.XmlType; - import org.apache.causeway.applib.annotation.Action; import org.apache.causeway.applib.annotation.ActionLayout; import org.apache.causeway.applib.annotation.Collection; import org.apache.causeway.applib.annotation.DomainObject; import org.apache.causeway.applib.annotation.Editing; +import org.apache.causeway.applib.annotation.Introspection; import org.apache.causeway.applib.annotation.MemberSupport; import org.apache.causeway.applib.annotation.Nature; import org.apache.causeway.applib.annotation.Optionality; @@ -42,6 +37,11 @@ import org.apache.causeway.extensions.fullcalendar.applib.value.CalendarEvent; import org.apache.causeway.extensions.fullcalendar.applib.value.CalendarEventSemantics; +import jakarta.inject.Named; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlRootElement; +import jakarta.xml.bind.annotation.XmlType; import lombok.Getter; import lombok.Setter; @@ -52,7 +52,7 @@ @XmlAccessorType(XmlAccessType.FIELD) @Named("testdomain.val.CalendarEventJaxbVm") @DomainObject( - nature=Nature.VIEW_MODEL) + nature=Nature.VIEW_MODEL, introspection = Introspection.ANNOTATION_REQUIRED) public class CalendarEventJaxbVm { @Property(editing = Editing.ENABLED) diff --git a/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/DomainModelTest_usingGoodDomain.java b/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/DomainModelTest_usingGoodDomain.java index e64f252ed9d..0e51500f257 100644 --- a/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/DomainModelTest_usingGoodDomain.java +++ b/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/DomainModelTest_usingGoodDomain.java @@ -18,21 +18,6 @@ */ package org.apache.causeway.testdomain.domainmodel; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import jakarta.inject.Inject; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -40,8 +25,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.TestPropertySource; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.causeway.applib.annotation.Introspection.EncapsulationPolicy; import org.apache.causeway.applib.annotation.Introspection.MemberAnnotationPolicy; @@ -111,6 +98,7 @@ import org.apache.causeway.testdomain.model.good.ProperMixinContribution_action4; import org.apache.causeway.testdomain.model.good.ProperMixinContribution_action5; import org.apache.causeway.testdomain.model.good.ProperMixinContribution_action6; +import org.apache.causeway.testdomain.model.good.ProperMixinContribution_actionRecord; import org.apache.causeway.testdomain.model.good.ProperObjectWithAlias; import org.apache.causeway.testdomain.model.good.ProperRecordAsViewModelWithAnnotationsOptional; import org.apache.causeway.testdomain.model.good.ProperRecordAsViewModelWithAnnotationsRequired; @@ -123,7 +111,18 @@ import org.apache.causeway.testdomain.util.interaction.DomainObjectTesterFactory; import org.apache.causeway.testing.integtestsupport.applib.CausewayIntegrationTestAbstract; import org.apache.causeway.testing.integtestsupport.applib.validate.DomainModelValidator; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import jakarta.inject.Inject; import lombok.RequiredArgsConstructor; @SpringBootTest( @@ -391,7 +390,7 @@ void elementTypes_shouldBeIntrospected_whenDiscoveredViaGenerics_usingNoWildcard var concreteCollSpec = concreteColl.getElementType(); assertEquals(ElementTypeConcrete.class, concreteCollSpec.getCorrespondingClass()); - assertEquals(BeanSort.VIEW_MODEL, concreteCollSpec.getBeanSort()); + assertEquals(BeanSort.VIEW_MODEL, concreteCollSpec.beanSort()); assertHasAction(concreteCollSpec, "abstractAction"); assertHasAction(concreteCollSpec, "interfaceAction"); assertHasProperty(concreteCollSpec, "abstractProp"); @@ -401,7 +400,7 @@ void elementTypes_shouldBeIntrospected_whenDiscoveredViaGenerics_usingNoWildcard var interfaceCollSpec = interfaceColl.getElementType(); assertEquals(ElementTypeInterface.class, interfaceCollSpec.getCorrespondingClass()); - assertEquals(BeanSort.ABSTRACT, interfaceCollSpec.getBeanSort()); + assertEquals(BeanSort.ABSTRACT, interfaceCollSpec.beanSort()); assertHasAction(interfaceCollSpec, "interfaceAction"); assertHasProperty(interfaceCollSpec, "interfaceProp"); @@ -409,7 +408,7 @@ void elementTypes_shouldBeIntrospected_whenDiscoveredViaGenerics_usingNoWildcard var abstractCollSpec = abstractColl.getElementType(); assertEquals(ElementTypeAbstract.class, abstractCollSpec.getCorrespondingClass()); - assertEquals(BeanSort.ABSTRACT, abstractCollSpec.getBeanSort()); + assertEquals(BeanSort.ABSTRACT, abstractCollSpec.beanSort()); assertHasAction(abstractCollSpec, "abstractAction"); assertHasProperty(abstractCollSpec, "abstractProp"); } @@ -430,7 +429,7 @@ void elementTypeInference_fromGenerics_usingNoWildcards() { var interfaceSetSpec = interfaceSet.getElementType(); assertEquals(ElementTypeInterface.class, interfaceSetSpec.getCorrespondingClass()); - assertEquals(BeanSort.ABSTRACT, interfaceSetSpec.getBeanSort()); + assertEquals(BeanSort.ABSTRACT, interfaceSetSpec.beanSort()); //TODO scenario 2 @@ -444,7 +443,7 @@ void elementTypeInference_fromGenerics_usingNoWildcards() { var interfaceIterSpec = interfaceIter.getElementType(); assertEquals(ElementTypeInterface.class, interfaceIterSpec.getCorrespondingClass()); - assertEquals(BeanSort.ABSTRACT, interfaceIterSpec.getBeanSort()); + assertEquals(BeanSort.ABSTRACT, interfaceIterSpec.beanSort()); } @@ -459,7 +458,7 @@ void elementTypes_shouldBeIntrospected_whenDiscoveredViaGenerics_usingWildcards( var concreteCollSpec = concreteColl.getElementType(); assertEquals(ElementTypeConcrete.class, concreteCollSpec.getCorrespondingClass()); - assertEquals(BeanSort.VIEW_MODEL, concreteCollSpec.getBeanSort()); + assertEquals(BeanSort.VIEW_MODEL, concreteCollSpec.beanSort()); assertHasAction(concreteCollSpec, "abstractAction"); assertHasAction(concreteCollSpec, "interfaceAction"); assertHasProperty(concreteCollSpec, "abstractProp"); @@ -469,7 +468,7 @@ void elementTypes_shouldBeIntrospected_whenDiscoveredViaGenerics_usingWildcards( var interfaceCollSpec = interfaceColl.getElementType(); assertEquals(ElementTypeInterface.class, interfaceCollSpec.getCorrespondingClass()); - assertEquals(BeanSort.ABSTRACT, interfaceCollSpec.getBeanSort()); + assertEquals(BeanSort.ABSTRACT, interfaceCollSpec.beanSort()); assertHasAction(interfaceCollSpec, "interfaceAction"); assertHasProperty(interfaceCollSpec, "interfaceProp"); @@ -477,7 +476,7 @@ void elementTypes_shouldBeIntrospected_whenDiscoveredViaGenerics_usingWildcards( var abstractCollSpec = abstractColl.getElementType(); assertEquals(ElementTypeAbstract.class, abstractCollSpec.getCorrespondingClass()); - assertEquals(BeanSort.ABSTRACT, abstractCollSpec.getBeanSort()); + assertEquals(BeanSort.ABSTRACT, abstractCollSpec.beanSort()); assertHasAction(abstractCollSpec, "abstractAction"); assertHasProperty(abstractCollSpec, "abstractProp"); @@ -497,7 +496,7 @@ void elementTypes_shouldBeIntrospected_whenDiscoveredViaGenerics_usingWildcards( void domainObjects_ifNatureNotSpecified_shouldConsiderBeanTypeClassifier() { var vmSpec = specificationLoader.specForTypeElseFail(ProperViewModelInferredFromNotBeingAnEntity.class); - assertEquals(BeanSort.VIEW_MODEL, vmSpec.getBeanSort()); + assertEquals(BeanSort.VIEW_MODEL, vmSpec.beanSort()); assertNotNull(vmSpec.lookupFacet(ViewModelFacet.class).orElse(null)); } @@ -506,7 +505,7 @@ void interfaces_shouldSupport_inheritedMembers() { var i2Spec = specificationLoader.specForTypeElseFail(ProperInterface2.class); - assertEquals(BeanSort.ABSTRACT, i2Spec.getBeanSort()); + assertEquals(BeanSort.ABSTRACT, i2Spec.beanSort()); assertHasProperty(i2Spec, "a"); assertHasProperty(i2Spec, "b"); assertHasProperty(i2Spec, "c"); @@ -684,7 +683,7 @@ void aliasesOnDomainServices_shouldBeHonored() { assertEquals(Can.of( "testdomain.v1.ProperServiceWithAlias", "testdomain.v2.ProperServiceWithAlias"), - objectSpec.getAliases().map(LogicalType::logicalName)); + objectSpec.aliases().map(LogicalType::logicalName)); assertEquals(objectSpec, specificationLoader.specForLogicalTypeName("testdomain.v1.ProperServiceWithAlias") @@ -704,7 +703,7 @@ void aliasesOnDomainObjects_shouldBeHonored() { assertEquals(Can.of( "testdomain.v1.ProperObjectWithAlias", "testdomain.v2.ProperObjectWithAlias"), - objectSpec.getAliases().map(LogicalType::logicalName)); + objectSpec.aliases().map(LogicalType::logicalName)); assertEquals(objectSpec, specificationLoader.specForLogicalTypeName("testdomain.v1.ProperObjectWithAlias") @@ -1006,6 +1005,26 @@ void mixins_shouldBePickedUp_asTheRightContributingFeature(final Class mixinC assertMissesProperty(vmSpec, actionName); // verify don't contributes as property } + // -- JAVA RECORD AS MIXIN + + @ParameterizedTest + @ValueSource(classes = { + ProperMixinContribution_actionRecord.class + }) + @Disabled("WIP") + void record_as_mixin(final Class mixinClass) { + + final String actionName = _Strings.splitThenStream(mixinClass.getSimpleName(), "_") + .reduce((a, b)->b) + .orElseThrow(); + + var mixinSpec = specificationLoader.specForTypeElseFail(mixinClass); + + var vmSpec = specificationLoader.specForTypeElseFail(ProperMixinContribution.class); + assertHasAction(vmSpec, actionName); // contributed action + assertMissesProperty(vmSpec, actionName); // verify don't contributes as property + } + // -- JAVA RECORD AS VIEWMODEL @RequiredArgsConstructor @@ -1035,7 +1054,7 @@ void javaRecordAsViewModel(final RecordScenario scenario) { var elementType = viewModel.objSpec(); var viewmodelFacet = elementType.lookupFacet(ViewModelFacet.class).orElse(null); - assertEquals(BeanSort.VIEW_MODEL, elementType.getBeanSort()); + assertEquals(BeanSort.VIEW_MODEL, elementType.beanSort()); assertEquals(classUnderTest.getName(), elementType.getFeatureIdentifier().logicalTypeName()); assertTrue(ViewModelFacetForJavaRecord.class.isInstance(viewmodelFacet), ()->"Record is expected to have a ViewModelFacetForJavaRecord, got %s" diff --git a/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.verify.approved.xml b/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.verify.approved.xml index 7ae8d07e0e1..3359aeb2447 100644 --- a/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.verify.approved.xml +++ b/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.verify.approved.xml @@ -56,8 +56,8 @@ - + @@ -189,1130 +189,6 @@ org.apache.causeway.testdomain.model.good.ProperMemberSupport - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - org.apache.causeway.applib.value.Blob - - - - - - - - - - - - - - - - - - - - - - - - - - - - java.lang.String - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - java.lang.Object - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - java.lang.String - - - - - - - - - - - - - - - - - - - - - - - - - - - - org.apache.causeway.applib.services.layout.LayoutExportStyle - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - org.apache.causeway.applib.value.NamedWithMimeType$CommonMimeType - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - java.lang.Object - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - java.lang.String - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - java.lang.Object - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - org.apache.causeway.applib.value.LocalResourcePath - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - java.lang.Object - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - org.apache.causeway.applib.Identifier - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - java.lang.String - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - java.lang.Object - - - - - - - - - - - - - - - - - - - - - - - - - - - java.lang.String - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - java.lang.Object - - @@ -6757,8 +5633,8 @@ - + @@ -8359,8 +7235,8 @@ - + @@ -34042,8 +32918,8 @@ - + @@ -34367,8 +33243,8 @@ - + @@ -34681,8 +33557,8 @@ - + @@ -35009,8 +33885,8 @@ - + @@ -35337,8 +34213,8 @@ - + @@ -35524,8 +34400,8 @@ - + @@ -35844,8 +34720,8 @@ - + @@ -36066,8 +34942,8 @@ - + @@ -36288,8 +35164,8 @@ - + @@ -36489,8 +35365,8 @@ - + @@ -36720,8 +35596,8 @@ - + @@ -36951,8 +35827,8 @@ - + @@ -39203,8 +38079,8 @@ - + @@ -39381,8 +38257,8 @@ - + @@ -39559,8 +38435,8 @@ - + @@ -39737,8 +38613,8 @@ - + @@ -39915,8 +38791,8 @@ - + @@ -40093,8 +38969,8 @@ - + @@ -40271,8 +39147,8 @@ - + @@ -40449,8 +39325,8 @@ - + @@ -49826,8 +48702,8 @@ - + diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/context/Context.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/context/Context.java index 3eef3831c94..b4c49274818 100644 --- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/context/Context.java +++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/context/Context.java @@ -18,6 +18,9 @@ */ package org.apache.causeway.viewer.graphql.model.context; +import static graphql.schema.GraphQLEnumType.newEnum; +import static graphql.schema.GraphQLEnumValueDefinition.newEnumValueDefinition; + import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; @@ -25,14 +28,6 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -import graphql.schema.GraphQLCodeRegistry; -import graphql.schema.GraphQLEnumType; - -import static graphql.schema.GraphQLEnumType.newEnum; -import static graphql.schema.GraphQLEnumValueDefinition.newEnumValueDefinition; - -import org.springframework.stereotype.Component; - import org.apache.causeway.applib.id.HasLogicalType; import org.apache.causeway.applib.services.bookmark.BookmarkService; import org.apache.causeway.applib.services.registry.ServiceRegistry; @@ -49,7 +44,10 @@ import org.apache.causeway.viewer.graphql.model.domain.common.query.CommonDomainService; import org.apache.causeway.viewer.graphql.model.registry.GraphQLTypeRegistry; import org.apache.causeway.viewer.graphql.model.types.TypeMapper; +import org.springframework.stereotype.Component; +import graphql.schema.GraphQLCodeRegistry; +import graphql.schema.GraphQLEnumType; import lombok.RequiredArgsConstructor; @Component @@ -97,7 +95,7 @@ public List objectSpecifications(final Predicate< x.isViewModel() || (includeEntities && x.isEntity()) || (includeEntities && x.isAbstract()) // this is a little bit inaccurate; Person.class was not being picked up, not sure that MappedSuperclass is enough to install the EntityFacet though. - || x.getBeanSort().isManagedBeanContributing() + || x.beanSort().isManagedBeanContributing() ) .filter(predicate) .sorted(Comparator.comparing(HasLogicalType::logicalTypeName)) @@ -105,9 +103,8 @@ public List objectSpecifications(final Predicate< } private void computeLogicalTypeNames() { - if (logicalTypeNames != null) { - return; - } + if (logicalTypeNames != null) + return; logicalTypeNames = doComputeLogicalTypeNames(); graphQLTypeRegistry.addTypeIfNotAlreadyPresent(logicalTypeNames); } diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/common/query/CommonMeta.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/common/query/CommonMeta.java index 7d0174ce4a9..138e1594a1d 100644 --- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/common/query/CommonMeta.java +++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/common/query/CommonMeta.java @@ -94,7 +94,7 @@ private boolean isResourceNotForbidden() { } private boolean isEntity() { - return holder.getObjectSpecification().getBeanSort() == BeanSort.ENTITY; + return holder.getObjectSpecification().beanSort() == BeanSort.ENTITY; } @Override diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/common/query/CommonTopLevelQueryAbstract.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/common/query/CommonTopLevelQueryAbstract.java index b7c448b15c5..9677bedc74e 100644 --- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/common/query/CommonTopLevelQueryAbstract.java +++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/common/query/CommonTopLevelQueryAbstract.java @@ -48,28 +48,26 @@ public CommonTopLevelQueryAbstract( this.schemaStrategy = schemaStrategy; context.objectSpecifications().forEach(objectSpec -> { - switch (objectSpec.getBeanSort()) { - - case ABSTRACT: - case VIEW_MODEL: - case ENTITY: + switch (objectSpec.beanSort()) { + case ABSTRACT, VIEW_MODEL, ENTITY -> { var gqlvDomainObject = schemaStrategy.domainObjectFor(objectSpec, context); addChildField(gqlvDomainObject.newField()); domainObjects.add(gqlvDomainObject); - break; - + } + default -> {} } }); // add services to top-level query context.objectSpecifications().forEach(objectSpec -> { - switch (objectSpec.getBeanSort()) { - case MANAGED_BEAN_CONTRIBUTING: // @DomainService + switch (objectSpec.beanSort()) { + case MANAGED_BEAN_CONTRIBUTING -> { // @DomainService context.serviceRegistry.lookupBeanById(objectSpec.logicalTypeName()) .ifPresent(servicePojo -> domainServices.add( addChildFieldFor(schemaStrategy.domainServiceFor(objectSpec, servicePojo, context)))); - break; + } + default -> {} } }); } diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/common/query/ObjectFeatureUtils.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/common/query/ObjectFeatureUtils.java index f84c6c7d7dd..7ccb2fc9761 100644 --- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/common/query/ObjectFeatureUtils.java +++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/common/query/ObjectFeatureUtils.java @@ -110,7 +110,7 @@ public static Can argumentManagedObjectsFor( Object argumentValue = argumentPojos.get(oap.asciiId()); Object pojoOrPojoList; - switch (elementType.getBeanSort()) { + switch (elementType.beanSort()) { case VALUE: return adaptValue(oap, argumentValue, context); @@ -145,7 +145,7 @@ public static Can argumentManagedObjectsFor( case UNKNOWN: default: throw new IllegalArgumentException(String.format( - "Cannot handle an input type for %s; beanSort is %s", elementType.getFullIdentifier(), elementType.getBeanSort())); + "Cannot handle an input type for %s; beanSort is %s", elementType.getFullIdentifier(), elementType.beanSort())); } }); } diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/mutation/RichMutationForAction.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/mutation/RichMutationForAction.java index 4f43941f888..195e1a3c23d 100644 --- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/mutation/RichMutationForAction.java +++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/mutation/RichMutationForAction.java @@ -18,21 +18,12 @@ */ package org.apache.causeway.viewer.graphql.model.domain.rich.mutation; +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; + import java.util.ArrayList; import java.util.Map; import java.util.Optional; -import graphql.schema.DataFetchingEnvironment; -import graphql.schema.GraphQLArgument; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLList; -import graphql.schema.GraphQLOutputType; -import graphql.schema.GraphQLType; - -import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; - -import org.jspecify.annotations.Nullable; - import org.apache.causeway.applib.annotation.Where; import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.commons.collections.Can; @@ -55,7 +46,14 @@ import org.apache.causeway.viewer.graphql.model.exceptions.HiddenException; import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo; import org.apache.causeway.viewer.graphql.model.types.TypeMapper; +import org.jspecify.annotations.Nullable; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLType; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -65,7 +63,7 @@ public class RichMutationForAction extends Element { private final ObjectSpecification objectSpec; private final ObjectAction objectAction; - private String argumentName; + private final String argumentName; public RichMutationForAction( final ObjectSpecification objectSpec, @@ -98,7 +96,7 @@ private static String fieldName( @Nullable private GraphQLOutputType typeFor(final ObjectAction objectAction){ ObjectSpecification objectSpecification = objectAction.getReturnType(); - switch (objectSpecification.getBeanSort()){ + switch (objectSpecification.beanSort()){ case COLLECTION: @@ -129,7 +127,7 @@ private GraphQLOutputType typeFor(final ObjectAction objectAction){ @Override protected Object fetchData(final DataFetchingEnvironment dataFetchingEnvironment) { - var isService = objectSpec.getBeanSort().isManagedBeanContributing(); + var isService = objectSpec.beanSort().isManagedBeanContributing(); var environment = new Environment.For(dataFetchingEnvironment); Object sourcePojo; @@ -159,9 +157,8 @@ protected Object fetchData(final DataFetchingEnvironment dataFetchingEnvironment String key = ObjectFeatureUtils.keyFor(refValue); BookmarkedPojo value = environment.getGraphQlContext().get(key); result = Optional.of(value).map(BookmarkedPojo::getTargetPojo); - } else { - throw new IllegalArgumentException("Either 'id' or 'ref' must be specified for a DomainObject input type"); - } + } else + throw new IllegalArgumentException("Either 'id' or 'ref' must be specified for a DomainObject input type"); } sourcePojo = result .orElseThrow(); // TODO: better error handling if no such object found. @@ -170,22 +167,19 @@ protected Object fetchData(final DataFetchingEnvironment dataFetchingEnvironment ManagedObject managedObject = ManagedObject.adaptSingular(objectSpec, sourcePojo); var visibleConsent = objectAction.isVisible(managedObject, InteractionInitiatedBy.USER, Where.ANYWHERE); - if (visibleConsent.isVetoed()) { - throw new HiddenException(objectAction.getFeatureIdentifier()); - } + if (visibleConsent.isVetoed()) + throw new HiddenException(objectAction.getFeatureIdentifier()); var usableConsent = objectAction.isUsable(managedObject, InteractionInitiatedBy.USER, Where.ANYWHERE); - if (usableConsent.isVetoed()) { - throw new DisabledException(objectAction.getFeatureIdentifier()); - } + if (usableConsent.isVetoed()) + throw new DisabledException(objectAction.getFeatureIdentifier()); var head = objectAction.interactionHead(managedObject); var argumentManagedObjects = argumentManagedObjectsFor(environment, objectAction); var validityConsent = objectAction.isArgumentSetValid(head, argumentManagedObjects, InteractionInitiatedBy.USER); - if (validityConsent.isVetoed()) { - throw new IllegalArgumentException(validityConsent.getReasonAsString().orElse("Invalid")); - } + if (validityConsent.isVetoed()) + throw new IllegalArgumentException(validityConsent.getReasonAsString().orElse("Invalid")); var resultManagedObject = objectAction.execute(head, argumentManagedObjects, InteractionInitiatedBy.USER); return resultManagedObject.getPojo(); @@ -198,7 +192,7 @@ private void addGqlArguments(final GraphQLFieldDefinition.Builder fieldBuilder) var argName = context.causewayConfiguration.viewer().graphql().mutation().targetArgName(); // add target (if not a service) - if (! objectSpec.getBeanSort().isManagedBeanContributing()) { + if (! objectSpec.beanSort().isManagedBeanContributing()) { arguments.add( GraphQLArgument.newArgument() .name(argName) diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/query/RichAction.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/query/RichAction.java index 5bee23a33bb..c87b8ecafc4 100644 --- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/query/RichAction.java +++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/query/RichAction.java @@ -23,9 +23,6 @@ import java.util.Optional; import java.util.stream.Collectors; -import graphql.schema.GraphQLArgument; -import graphql.schema.GraphQLFieldDefinition; - import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.applib.services.bookmark.BookmarkService; import org.apache.causeway.commons.collections.Can; @@ -46,6 +43,8 @@ import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo; import org.apache.causeway.viewer.graphql.model.types.TypeMapper; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLFieldDefinition; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -95,16 +94,12 @@ public RichAction( private boolean isInvokeAllowed(final ObjectAction objectAction) { var apiVariant = context.causewayConfiguration.viewer().graphql().apiVariant(); - switch (apiVariant) { - case QUERY_ONLY: - case QUERY_AND_MUTATIONS: - return objectAction.getSemantics().isSafeInNature(); - case QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT: - return true; - default: - // shouldn't happen - throw new IllegalArgumentException("Unknown API variant: " + apiVariant); - } + return switch (apiVariant) { + case QUERY_ONLY, QUERY_AND_MUTATIONS -> objectAction.getSemantics().isSafeInNature(); + case QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT -> true; + default -> // shouldn't happen + throw new IllegalArgumentException("Unknown API variant: " + apiVariant); + }; } @Override @@ -135,17 +130,15 @@ public static Can argumentManagedObjectsFor( Object argumentValue = argumentPojos.get(oap.asciiId()); Object pojoOrPojoList; - switch (elementType.getBeanSort()) { + switch (elementType.beanSort()) { case VALUE: return adaptValue(oap, argumentValue, context); case ENTITY: case VIEW_MODEL: - if (argumentValue == null) { - return ManagedObject.empty(elementType); - } - // fall through + if (argumentValue == null) + return ManagedObject.empty(elementType); case ABSTRACT: // if the parameter is abstract, we still attempt to figure out the arguments. @@ -170,7 +163,7 @@ public static Can argumentManagedObjectsFor( case UNKNOWN: default: throw new IllegalArgumentException(String.format( - "Cannot handle an input type for %s; beanSort is %s", elementType.getFullIdentifier(), elementType.getBeanSort())); + "Cannot handle an input type for %s; beanSort is %s", elementType.getFullIdentifier(), elementType.beanSort())); } }); } @@ -181,9 +174,8 @@ private static ManagedObject adaptValue( final Context context) { var elementType = oap.getElementType(); - if (argumentValue == null) { - return ManagedObject.empty(elementType); - } + if (argumentValue == null) + return ManagedObject.empty(elementType); var argPojo = context.typeMapper.unmarshal(argumentValue, elementType); return ManagedObject.adaptParameter(oap, argPojo); @@ -201,22 +193,19 @@ public static Optional asPojo( if (refValue != null) { String key = ObjectFeatureUtils.keyFor(refValue); BookmarkedPojo bookmarkedPojo = environment.getGraphQlContext().get(key); - if (bookmarkedPojo == null) { - throw new IllegalArgumentException(String.format( + if (bookmarkedPojo == null) + throw new IllegalArgumentException(String.format( "Could not find object referenced '%s' in the execution context; was it saved previously using \"saveAs\" ?", refValue)); - } var targetPojoClass = bookmarkedPojo.getTargetPojo().getClass(); var targetPojoSpec = context.specificationLoader.loadSpecification(targetPojoClass); - if (targetPojoSpec == null) { - throw new IllegalArgumentException(String.format( + if (targetPojoSpec == null) + throw new IllegalArgumentException(String.format( "The object referenced '%s' is not part of the metamodel (has class '%s')", refValue, targetPojoClass.getCanonicalName())); - } - if (!elementType.isPojoCompatible(bookmarkedPojo.getTargetPojo())) { - throw new IllegalArgumentException(String.format( + if (!elementType.isPojoCompatible(bookmarkedPojo.getTargetPojo())) + throw new IllegalArgumentException(String.format( "The object referenced '%s' has a type '%s' that is not assignable to the required type '%s'", refValue, targetPojoSpec.logicalTypeName(), elementType.logicalTypeName())); - } return Optional.of(bookmarkedPojo).map(BookmarkedPojo::getTargetPojo); } @@ -226,11 +215,10 @@ public static Optional asPojo( Optional bookmarkIfAny; if(elementType.isAbstract()) { var objectSpecArg = (ObjectSpecification)argumentValue.get("logicalTypeName"); - if (objectSpecArg == null) { - throw new IllegalArgumentException(String.format( + if (objectSpecArg == null) + throw new IllegalArgumentException(String.format( "The 'logicalTypeName' is required along with the 'id', because the input type '%s' is abstract", elementType.logicalTypeName())); - } bookmarkIfAny = Optional.of(Bookmark.forLogicalTypeNameAndIdentifier(objectSpecArg.logicalTypeName(), idValue)); } else { bookmarkIfAny = context.bookmarkService.bookmarkFor(paramClass, idValue); @@ -291,9 +279,8 @@ public ObjectSpecification getObjectSpecification() { @Override protected void addDataFetchersForChildren() { - if(hidden == null) { - return; - } + if(hidden == null) + return; hidden.addDataFetcher(this); disabled.addDataFetcher(this); validate.addDataFetcher(this); diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/query/RichActionInvokeResult.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/query/RichActionInvokeResult.java index 249f06d01b7..bfa159db84d 100644 --- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/query/RichActionInvokeResult.java +++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/query/RichActionInvokeResult.java @@ -18,15 +18,8 @@ */ package org.apache.causeway.viewer.graphql.model.domain.rich.query; -import graphql.schema.DataFetchingEnvironment; -import graphql.schema.GraphQLList; -import graphql.schema.GraphQLOutputType; -import graphql.schema.GraphQLType; - import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; -import org.jspecify.annotations.Nullable; - import org.apache.causeway.applib.annotation.Where; import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy; import org.apache.causeway.core.metamodel.facets.actcoll.typeof.TypeOfFacet; @@ -39,7 +32,12 @@ import org.apache.causeway.viewer.graphql.model.exceptions.DisabledException; import org.apache.causeway.viewer.graphql.model.exceptions.HiddenException; import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo; +import org.jspecify.annotations.Nullable; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLType; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -70,7 +68,7 @@ public RichActionInvokeResult( @Nullable private GraphQLOutputType typeFor(final ObjectAction objectAction){ var objectSpecification = objectAction.getReturnType(); - switch (objectSpecification.getBeanSort()){ + switch (objectSpecification.beanSort()){ case COLLECTION: @@ -106,30 +104,26 @@ protected Object fetchData(final DataFetchingEnvironment dataFetchingEnvironment var environment = new Environment.ForTunnelled(dataFetchingEnvironment); var objectSpecification = context.specificationLoader.loadSpecification(sourcePojo.getClass()); - if (objectSpecification == null) { - return null; - } + if (objectSpecification == null) + return null; var objectAction = actionInteractor.getObjectMember(); var managedObject = ManagedObject.adaptSingular(objectSpecification, sourcePojo); var visibleConsent = objectAction.isVisible(managedObject, InteractionInitiatedBy.USER, Where.ANYWHERE); - if (visibleConsent.isVetoed()) { - throw new HiddenException(objectAction.getFeatureIdentifier()); - } + if (visibleConsent.isVetoed()) + throw new HiddenException(objectAction.getFeatureIdentifier()); var usableConsent = objectAction.isUsable(managedObject, InteractionInitiatedBy.USER, Where.ANYWHERE); - if (usableConsent.isVetoed()) { - throw new DisabledException(objectAction.getFeatureIdentifier()); - } + if (usableConsent.isVetoed()) + throw new DisabledException(objectAction.getFeatureIdentifier()); var head = objectAction.interactionHead(managedObject); var argumentManagedObjects = actionInteractor.argumentManagedObjectsFor(environment, objectAction, context.bookmarkService); var validityConsent = objectAction.isArgumentSetValid(head, argumentManagedObjects, InteractionInitiatedBy.USER); - if (validityConsent.isVetoed()) { - throw new IllegalArgumentException(validityConsent.getReasonAsString().orElse("Invalid")); - } + if (validityConsent.isVetoed()) + throw new IllegalArgumentException(validityConsent.getReasonAsString().orElse("Invalid")); var resultManagedObject = objectAction.execute(head, argumentManagedObjects, InteractionInitiatedBy.USER); return resultManagedObject.getPojo(); diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/scenario/ScenarioStep.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/scenario/ScenarioStep.java index 44c1b265fb6..a977f183e87 100644 --- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/scenario/ScenarioStep.java +++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/scenario/ScenarioStep.java @@ -22,8 +22,6 @@ import java.util.List; import java.util.Objects; -import graphql.schema.DataFetchingEnvironment; - import org.apache.causeway.applib.services.metamodel.BeanSort; import org.apache.causeway.viewer.graphql.model.context.Context; import org.apache.causeway.viewer.graphql.model.domain.ElementCustom; @@ -32,6 +30,8 @@ import org.apache.causeway.viewer.graphql.model.domain.common.query.CommonDomainObject; import org.apache.causeway.viewer.graphql.model.domain.common.query.CommonDomainService; +import graphql.schema.DataFetchingEnvironment; + public class ScenarioStep extends ElementCustom implements Parent { @@ -44,28 +44,25 @@ public ScenarioStep( final Context context) { super("ScenarioStep", context); - if(isBuilt()) { - return; - } + if(isBuilt()) + return; // add domain object lookup to top-level query context.objectSpecifications().forEach(objectSpec -> { - switch (objectSpec.getBeanSort()) { - - case ABSTRACT: - case VIEW_MODEL: // @DomainObject(nature=VIEW_MODEL) - case ENTITY: // @DomainObject(nature=ENTITY) - + switch (objectSpec.beanSort()) { + case ABSTRACT, + VIEW_MODEL, // @DomainObject(nature=VIEW_MODEL) + ENTITY -> { // @DomainObject(nature=ENTITY) var gqlvDomainObject = schemaStrategy.domainObjectFor(objectSpec, context); addChildField(gqlvDomainObject.newField()); domainObjects.add(gqlvDomainObject); - - break; + } + default -> {} } }); context.objectSpecifications().forEach(objectSpec -> { - if (Objects.requireNonNull(objectSpec.getBeanSort()) == BeanSort.MANAGED_BEAN_CONTRIBUTING) { // @DomainService + if (Objects.requireNonNull(objectSpec.beanSort()) == BeanSort.MANAGED_BEAN_CONTRIBUTING) { // @DomainService context.serviceRegistry.lookupBeanById(objectSpec.logicalTypeName()) .ifPresent(servicePojo -> domainServices.add(addChildFieldFor(schemaStrategy.domainServiceFor(objectSpec, servicePojo, context)))); } diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/simple/mutation/SimpleMutationForAction.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/simple/mutation/SimpleMutationForAction.java index 9c4adf2bbc4..0e304b3b9ee 100644 --- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/simple/mutation/SimpleMutationForAction.java +++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/simple/mutation/SimpleMutationForAction.java @@ -18,21 +18,12 @@ */ package org.apache.causeway.viewer.graphql.model.domain.simple.mutation; +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; + import java.util.ArrayList; import java.util.Map; import java.util.Optional; -import graphql.schema.DataFetchingEnvironment; -import graphql.schema.GraphQLArgument; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLList; -import graphql.schema.GraphQLOutputType; -import graphql.schema.GraphQLType; - -import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; - -import org.jspecify.annotations.Nullable; - import org.apache.causeway.applib.annotation.Where; import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.commons.collections.Can; @@ -54,7 +45,14 @@ import org.apache.causeway.viewer.graphql.model.exceptions.HiddenException; import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo; import org.apache.causeway.viewer.graphql.model.types.TypeMapper; +import org.jspecify.annotations.Nullable; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLType; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -64,7 +62,7 @@ public class SimpleMutationForAction extends Element { private final ObjectSpecification objectSpec; private final ObjectAction objectAction; - private String argumentName; + private final String argumentName; public SimpleMutationForAction( final ObjectSpecification objectSpec, @@ -97,7 +95,7 @@ private static String fieldName( @Nullable private GraphQLOutputType typeFor(final ObjectAction objectAction){ ObjectSpecification objectSpecification = objectAction.getReturnType(); - switch (objectSpecification.getBeanSort()){ + switch (objectSpecification.beanSort()){ case COLLECTION: @@ -128,7 +126,7 @@ private GraphQLOutputType typeFor(final ObjectAction objectAction){ @Override protected Object fetchData(final DataFetchingEnvironment dataFetchingEnvironment) { - var isService = objectSpec.getBeanSort().isManagedBeanContributing(); + var isService = objectSpec.beanSort().isManagedBeanContributing(); var environment = new Environment.For(dataFetchingEnvironment); Object sourcePojo; @@ -158,9 +156,8 @@ protected Object fetchData(final DataFetchingEnvironment dataFetchingEnvironment var key = ObjectFeatureUtils.keyFor(refValue); BookmarkedPojo value = environment.getGraphQlContext().get(key); result = Optional.of(value).map(BookmarkedPojo::getTargetPojo); - } else { - throw new IllegalArgumentException("Either 'id' or 'ref' must be specified for a DomainObject input type"); - } + } else + throw new IllegalArgumentException("Either 'id' or 'ref' must be specified for a DomainObject input type"); } sourcePojo = result .orElseThrow(); // TODO: better error handling if no such object found. @@ -169,22 +166,19 @@ protected Object fetchData(final DataFetchingEnvironment dataFetchingEnvironment ManagedObject managedObject = ManagedObject.adaptSingular(objectSpec, sourcePojo); var visibleConsent = objectAction.isVisible(managedObject, InteractionInitiatedBy.USER, Where.ANYWHERE); - if (visibleConsent.isVetoed()) { - throw new HiddenException(objectAction.getFeatureIdentifier()); - } + if (visibleConsent.isVetoed()) + throw new HiddenException(objectAction.getFeatureIdentifier()); var usableConsent = objectAction.isUsable(managedObject, InteractionInitiatedBy.USER, Where.ANYWHERE); - if (usableConsent.isVetoed()) { - throw new DisabledException(objectAction.getFeatureIdentifier()); - } + if (usableConsent.isVetoed()) + throw new DisabledException(objectAction.getFeatureIdentifier()); var head = objectAction.interactionHead(managedObject); var argumentManagedObjects = argumentManagedObjectsFor(environment, objectAction); var validityConsent = objectAction.isArgumentSetValid(head, argumentManagedObjects, InteractionInitiatedBy.USER); - if (validityConsent.isVetoed()) { - throw new IllegalArgumentException(validityConsent.getReasonAsString().orElse("Invalid")); - } + if (validityConsent.isVetoed()) + throw new IllegalArgumentException(validityConsent.getReasonAsString().orElse("Invalid")); var resultManagedObject = objectAction.execute(head, argumentManagedObjects, InteractionInitiatedBy.USER); return resultManagedObject.getPojo(); @@ -197,7 +191,7 @@ private void addGqlArguments(final GraphQLFieldDefinition.Builder fieldBuilder) var argName = context.causewayConfiguration.viewer().graphql().mutation().targetArgName(); // add target (if not a service) - if (! objectSpec.getBeanSort().isManagedBeanContributing()) { + if (! objectSpec.beanSort().isManagedBeanContributing()) { arguments.add( GraphQLArgument.newArgument() .name(argName) diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/simple/query/SimpleAction.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/simple/query/SimpleAction.java index 981bb3ee2c7..26ad504e515 100644 --- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/simple/query/SimpleAction.java +++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/simple/query/SimpleAction.java @@ -18,20 +18,13 @@ */ package org.apache.causeway.viewer.graphql.model.domain.simple.query; +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; + import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; -import graphql.schema.DataFetchingEnvironment; -import graphql.schema.GraphQLArgument; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLList; -import graphql.schema.GraphQLOutputType; -import graphql.schema.GraphQLType; - -import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; - import org.apache.causeway.applib.annotation.Where; import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.applib.services.bookmark.BookmarkService; @@ -54,6 +47,12 @@ import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo; import org.apache.causeway.viewer.graphql.model.types.TypeMapper; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLType; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -91,7 +90,7 @@ public String getId() { private GraphQLOutputType typeFor(final ObjectAction objectAction){ var objectSpecification = objectAction.getReturnType(); - switch (objectSpecification.getBeanSort()){ + switch (objectSpecification.beanSort()){ case COLLECTION: @@ -145,17 +144,15 @@ public static Can argumentManagedObjectsFor( Object argumentValue = argumentPojos.get(oap.asciiId()); Object pojoOrPojoList; - switch (elementType.getBeanSort()) { + switch (elementType.beanSort()) { case VALUE: return adaptValue(oap, argumentValue, context); case ENTITY: case VIEW_MODEL: - if (argumentValue == null) { - return ManagedObject.empty(elementType); - } - // fall through + if (argumentValue == null) + return ManagedObject.empty(elementType); case ABSTRACT: // if the parameter is abstract, we still attempt to figure out the arguments. @@ -180,7 +177,7 @@ public static Can argumentManagedObjectsFor( case UNKNOWN: default: throw new IllegalArgumentException(String.format( - "Cannot handle an input type for %s; beanSort is %s", elementType.getFullIdentifier(), elementType.getBeanSort())); + "Cannot handle an input type for %s; beanSort is %s", elementType.getFullIdentifier(), elementType.beanSort())); } }); } @@ -191,9 +188,8 @@ private static ManagedObject adaptValue( final Context context) { var elementType = oap.getElementType(); - if (argumentValue == null) { - return ManagedObject.empty(elementType); - } + if (argumentValue == null) + return ManagedObject.empty(elementType); var argPojo = context.typeMapper.unmarshal(argumentValue, elementType); return ManagedObject.adaptParameter(oap, argPojo); @@ -211,22 +207,19 @@ public static Optional asPojo( if (refValue != null) { String key = ObjectFeatureUtils.keyFor(refValue); BookmarkedPojo bookmarkedPojo = environment.getGraphQlContext().get(key); - if (bookmarkedPojo == null) { - throw new IllegalArgumentException(String.format( + if (bookmarkedPojo == null) + throw new IllegalArgumentException(String.format( "Could not find object referenced '%s' in the execution context; was it saved previously using \"saveAs\" ?", refValue)); - } var targetPojoClass = bookmarkedPojo.getTargetPojo().getClass(); var targetPojoSpec = context.specificationLoader.loadSpecification(targetPojoClass); - if (targetPojoSpec == null) { - throw new IllegalArgumentException(String.format( + if (targetPojoSpec == null) + throw new IllegalArgumentException(String.format( "The object referenced '%s' is not part of the metamodel (has class '%s')", refValue, targetPojoClass.getCanonicalName())); - } - if (!elementType.isPojoCompatible(bookmarkedPojo.getTargetPojo())) { - throw new IllegalArgumentException(String.format( + if (!elementType.isPojoCompatible(bookmarkedPojo.getTargetPojo())) + throw new IllegalArgumentException(String.format( "The object referenced '%s' has a type '%s' that is not assignable to the required type '%s'", refValue, targetPojoSpec.logicalTypeName(), elementType.logicalTypeName())); - } return Optional.of(bookmarkedPojo).map(BookmarkedPojo::getTargetPojo); } @@ -236,11 +229,10 @@ public static Optional asPojo( Optional bookmarkIfAny; if(elementType.isAbstract()) { var objectSpecArg = (ObjectSpecification)argumentValue.get("logicalTypeName"); - if (objectSpecArg == null) { - throw new IllegalArgumentException(String.format( + if (objectSpecArg == null) + throw new IllegalArgumentException(String.format( "The 'logicalTypeName' is required along with the 'id', because the input type '%s' is abstract", elementType.logicalTypeName())); - } bookmarkIfAny = Optional.of(Bookmark.forLogicalTypeNameAndIdentifier(objectSpecArg.logicalTypeName(), idValue)); } else { bookmarkIfAny = context.bookmarkService.bookmarkFor(paramClass, idValue); @@ -301,30 +293,26 @@ protected Object fetchData(final DataFetchingEnvironment dataFetchingEnvironment var environment = new Environment.For(dataFetchingEnvironment); var objectSpecification = context.specificationLoader.loadSpecification(sourcePojo.getClass()); - if (objectSpecification == null) { - return null; - } + if (objectSpecification == null) + return null; var objectAction = getObjectMember(); var managedObject = ManagedObject.adaptSingular(objectSpecification, sourcePojo); var visibleConsent = objectAction.isVisible(managedObject, InteractionInitiatedBy.USER, Where.ANYWHERE); - if (visibleConsent.isVetoed()) { - throw new HiddenException(objectAction.getFeatureIdentifier()); - } + if (visibleConsent.isVetoed()) + throw new HiddenException(objectAction.getFeatureIdentifier()); var usableConsent = objectAction.isUsable(managedObject, InteractionInitiatedBy.USER, Where.ANYWHERE); - if (usableConsent.isVetoed()) { - throw new DisabledException(objectAction.getFeatureIdentifier()); - } + if (usableConsent.isVetoed()) + throw new DisabledException(objectAction.getFeatureIdentifier()); var head = objectAction.interactionHead(managedObject); var argumentManagedObjects = argumentManagedObjectsFor(environment, objectAction, context.bookmarkService); var validityConsent = objectAction.isArgumentSetValid(head, argumentManagedObjects, InteractionInitiatedBy.USER); - if (validityConsent.isVetoed()) { - throw new IllegalArgumentException(validityConsent.getReasonAsString().orElse("Invalid")); - } + if (validityConsent.isVetoed()) + throw new IllegalArgumentException(validityConsent.getReasonAsString().orElse("Invalid")); var resultManagedObject = objectAction.execute(head, argumentManagedObjects, InteractionInitiatedBy.USER); return resultManagedObject.getPojo(); diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/types/TypeMapperDefault.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/types/TypeMapperDefault.java index e2c8b667704..ada823ad183 100644 --- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/types/TypeMapperDefault.java +++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/types/TypeMapperDefault.java @@ -21,14 +21,6 @@ import static graphql.schema.GraphQLNonNull.nonNull; import static graphql.schema.GraphQLTypeReference.typeRef; -import jakarta.inject.Inject; -import jakarta.inject.Provider; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.jspecify.annotations.Nullable; - import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.OneToManyActionParameter; import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation; @@ -36,12 +28,18 @@ import org.apache.causeway.viewer.graphql.model.context.Context; import org.apache.causeway.viewer.graphql.model.domain.SchemaType; import org.apache.causeway.viewer.graphql.model.domain.TypeNames; +import org.jspecify.annotations.Nullable; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import graphql.Scalars; import graphql.schema.GraphQLInputType; import graphql.schema.GraphQLList; import graphql.schema.GraphQLOutputType; import graphql.schema.GraphQLTypeReference; +import jakarta.inject.Inject; +import jakarta.inject.Provider; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor(onConstructor_ = {@Inject}) @@ -62,17 +60,15 @@ public TypeMapper defaultTypeMapper(final ScalarMapper scalarMapper, final Provi @Override public GraphQLOutputType outputTypeFor(final Class clazz){ - if (clazz.isEnum()) { - return contextProvider.get().graphQLTypeRegistry.addEnumTypeIfNotAlreadyPresent(clazz, SchemaType.RICH); - } + if (clazz.isEnum()) + return contextProvider.get().graphQLTypeRegistry.addEnumTypeIfNotAlreadyPresent(clazz, SchemaType.RICH); return scalarMapper.scalarTypeFor(clazz); } @Override public GraphQLInputType inputTypeFor(final Class clazz){ - if (clazz.isEnum()) { - return contextProvider.get().graphQLTypeRegistry.addEnumTypeIfNotAlreadyPresent(clazz, SchemaType.RICH); - } + if (clazz.isEnum()) + return contextProvider.get().graphQLTypeRegistry.addEnumTypeIfNotAlreadyPresent(clazz, SchemaType.RICH); return scalarMapper.scalarTypeFor(clazz); } @@ -81,9 +77,8 @@ public Object unmarshal( final Object gqlValue, final ObjectSpecification targetObjectSpec) { var correspondingClass = targetObjectSpec.getCorrespondingClass(); - if (correspondingClass.isEnum()) { - return gqlValue; - } + if (correspondingClass.isEnum()) + return gqlValue; return scalarMapper.unmarshal(gqlValue, correspondingClass); } @@ -93,21 +88,21 @@ public GraphQLOutputType outputTypeFor( final SchemaType schemaType) { ObjectSpecification otoaObjectSpec = oneToOneFeature.getElementType(); - return switch (otoaObjectSpec.getBeanSort()) { + return switch (otoaObjectSpec.beanSort()) { case VIEW_MODEL, ENTITY -> typeRefPossiblyOptional(oneToOneFeature, schemaType, otoaObjectSpec); case VALUE-> scalarTypePossiblyOptional(oneToOneFeature, otoaObjectSpec); default -> null; }; } - private static GraphQLOutputType typeRefPossiblyOptional(OneToOneFeature oneToOneFeature, SchemaType schemaType, ObjectSpecification otoaObjectSpec) { + private static GraphQLOutputType typeRefPossiblyOptional(final OneToOneFeature oneToOneFeature, final SchemaType schemaType, final ObjectSpecification otoaObjectSpec) { GraphQLTypeReference fieldTypeRef = typeRef(TypeNames.objectTypeNameFor(otoaObjectSpec, schemaType)); return oneToOneFeature.isOptional() ? fieldTypeRef : nonNull(fieldTypeRef); } - private GraphQLOutputType scalarTypePossiblyOptional(OneToOneFeature oneToOneFeature, ObjectSpecification otoaObjectSpec) { + private GraphQLOutputType scalarTypePossiblyOptional(final OneToOneFeature oneToOneFeature, final ObjectSpecification otoaObjectSpec) { GraphQLOutputType scalarType = outputTypeFor(otoaObjectSpec.getCorrespondingClass()); return oneToOneFeature.isOptional() ? scalarType @@ -120,7 +115,7 @@ public GraphQLOutputType outputTypeFor( final ObjectSpecification objectSpecification, final SchemaType schemaType){ - return switch (objectSpecification.getBeanSort()){ + return switch (objectSpecification.beanSort()){ case ABSTRACT, VIEW_MODEL, ENTITY -> typeRef(TypeNames.objectTypeNameFor(objectSpecification, schemaType)); case VALUE -> outputTypeFor(objectSpecification.getCorrespondingClass()); case COLLECTION -> null; // should be noop @@ -140,12 +135,12 @@ public GraphQLOutputType outputTypeFor( @Nullable public GraphQLList listTypeFor( final ObjectSpecification elementType, final SchemaType schemaType) { - return switch (elementType.getBeanSort()) { + return switch (elementType.beanSort()) { case VIEW_MODEL, ENTITY -> GraphQLList.list(typeRef(TypeNames.objectTypeNameFor(elementType, schemaType))); - case VALUE -> + case VALUE -> GraphQLList.list(outputTypeFor(elementType.getCorrespondingClass())); - default -> null; + default -> null; }; } @@ -154,7 +149,7 @@ public GraphQLInputType inputTypeFor( final OneToOneFeature oneToOneFeature, final InputContext inputContext, final SchemaType schemaType) { - + return oneToOneFeature.isOptional() || inputContext.isOptionalAlwaysAllowed() ? inputTypeFor_(oneToOneFeature, schemaType) : nonNull(inputTypeFor_(oneToOneFeature, schemaType)); @@ -164,14 +159,14 @@ private GraphQLInputType inputTypeFor_( final OneToOneFeature oneToOneFeature, final SchemaType schemaType){ var elementObjectSpec = oneToOneFeature.getElementType(); - + { // guard introduced to intercept interfaces, which otherwise seem to break schema creation // due to missing type reference for given name var elementClass = elementObjectSpec.getCorrespondingClass(); if(elementClass.isInterface()) return inputTypeFor(elementClass); } - - return switch (elementObjectSpec.getBeanSort()) { + + return switch (elementObjectSpec.beanSort()) { case ABSTRACT, VIEW_MODEL, ENTITY -> typeRef(TypeNames.inputTypeNameFor(elementObjectSpec, schemaType)); case VALUE -> inputTypeFor(elementObjectSpec.getCorrespondingClass()); case COLLECTION -> @@ -192,10 +187,10 @@ public GraphQLList inputTypeFor( public GraphQLInputType inputTypeFor( final ObjectSpecification elementType, final SchemaType schemaType){ - return switch (elementType.getBeanSort()) { + return switch (elementType.beanSort()) { case ABSTRACT, VIEW_MODEL, ENTITY -> typeRef(TypeNames.inputTypeNameFor(elementType, schemaType)); case VALUE -> inputTypeFor(elementType.getCorrespondingClass()); - case COLLECTION -> + case COLLECTION -> throw new IllegalArgumentException(String.format("ObjectSpec '%s' is not expected to have a beanSort of COLLECTION", elementType.getFullIdentifier())); default -> Scalars.GraphQLString; // for now }; diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ParameterModel.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ParameterModel.java index 761283b8428..bf8e586a109 100644 --- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ParameterModel.java +++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ParameterModel.java @@ -28,9 +28,9 @@ import org.apache.causeway.viewer.commons.model.attrib.HasUiParameter; import org.apache.causeway.viewer.commons.model.attrib.UiParameter; import org.apache.causeway.viewer.wicket.model.models.interaction.act.UiParameterWkt; +import org.jspecify.annotations.NonNull; import lombok.Getter; -import org.jspecify.annotations.NonNull; /** * Wraps a {@link UiParameterWkt}. @@ -66,9 +66,8 @@ private ParameterModel( public String validate(final @NonNull ManagedObject proposedArg) { //TODO[CAUSEWAY-3764] workaround for org.apache.wicket.markup.html.form.upload.FileUpload leaking into the meta-model // find the root cause then clean-up - if(proposedArg.objSpec().getBeanSort().isUnknown()) { - return null; - } + if(proposedArg.objSpec().beanSort().isUnknown()) + return null; proposedValue().getValue().setValue(proposedArg); // updates the pending parameter value //TODO [CAUSEWAY-3753] for some reason the ParameterModel.isValidationFeedbackActive() flag is not yet active, diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/actionlink/ActionLink.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/actionlink/ActionLink.java index 045e0d44841..8b5a495b7af 100644 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/actionlink/ActionLink.java +++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/actionlink/ActionLink.java @@ -223,7 +223,7 @@ private void startDialogWithParams(final AjaxRequestTarget target) { var actionOwnerSpec = actionModel.getActionOwner().objSpec(); var actionPrompt = ActionPromptProvider .getFrom(this.getPage()) - .getActionPrompt(actionModel.getPromptStyle(), actionOwnerSpec.getBeanSort()); + .getActionPrompt(actionModel.getPromptStyle(), actionOwnerSpec.beanSort()); var actionParametersPanel = getComponentFactoryRegistry() .createComponent(actionPrompt.getContentId(),