From 5405364bb814f95333c3e46f3bac51a44412814f Mon Sep 17 00:00:00 2001 From: andi-huber Date: Tue, 28 Jul 2026 09:09:08 +0200 Subject: [PATCH 01/22] CAUSEWAY-4044: work on ObjectSpecificationDefault Task-Url: https://issues.apache.org/jira/browse/CAUSEWAY-4044 --- .../config/beans/CausewayBeanMetaData.java | 22 +- ...ternOptionalStringConstraintValidator.java | 50 +- .../core/metamodel/spec/Hierarchical.java | 52 ++ .../spec/ObjectSpecificationRecord.java | 217 ++++- .../metamodel/spec/impl/FacetProcessor.java | 18 +- .../spec/impl/FacetedMethodsBuilder.java | 27 +- .../metamodel/spec/impl/MemberPopulator.java | 85 ++ .../spec/impl/MixedInMemberFactory.java | 126 +++ .../spec/impl/ObjectSpecificationDefault.java | 324 +------ .../spec/impl/RegularMemberFactory.java | 78 ++ .../impl/SpecificationLoaderInternal.java | 5 +- .../spec/impl/_MemberSortingUtils.java | 24 +- .../metamodel/spec/impl/MemberPopulator2.java | 379 ++++++++ .../spec/impl/ObjectSpecification2.java | 821 ++++++++++++++++++ .../object/mixin/MixinFacetAbstract_Test.java | 22 + .../IntrospectionState_comparable_Test.java | 9 +- .../RuntimeServicesTestAbstract.java | 9 +- .../ProperMixinContribution_actionRecord.java | 36 + .../DomainModelTest_usingGoodDomain.java | 22 + 19 files changed, 1959 insertions(+), 367 deletions(-) create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator.java create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixedInMemberFactory.java create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/RegularMemberFactory.java create mode 100644 core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator2.java create mode 100644 core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecification2.java create mode 100644 regressiontests/base/src/main/java/org/apache/causeway/testdomain/model/good/ProperMixinContribution_actionRecord.java 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/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/spec/Hierarchical.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/Hierarchical.java index b37787476e2..38cda693baf 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,16 @@ */ 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.applib.annotation.Domain; 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 +62,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/ObjectSpecificationRecord.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecificationRecord.java index 1f2dd43277e..23181a5722f 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,114 @@ */ package org.apache.causeway.core.metamodel.spec; +import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Stream; +import org.apache.causeway.applib.annotation.DomainService; +import org.apache.causeway.applib.annotation.Introspection.IntrospectionPolicy; +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.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.base._Lazy; +import org.apache.causeway.commons.internal.reflection._ClassCache; +import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; +import org.apache.causeway.core.config.beans.CausewayBeanMetaData; +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.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.all.hide.HiddenFacet; +import org.apache.causeway.core.metamodel.facets.object.entity.EntityFacet; +import org.apache.causeway.core.metamodel.facets.object.immutable.ImmutableFacet; +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.parented.ParentedCollectionFacet; +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.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.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; //TODO[causeway-core-metamodel-CAUSEWAY-3834] WIP public record ObjectSpecificationRecord( - LogicalType logicalType, + CausewayBeanMetaData typeMeta, FeatureType featureType, FacetHolder facetHolder, Hierarchical hierarchical, ObjectActionContainer actionContainer, - ObjectAssociationContainer associationContainer) + ObjectAssociationContainer associationContainer, + IntrospectionPolicy introspectionPolicy, + Optional> valueFacet, + Optional entityFacet, + Optional viewmodelFacet, + Optional mixinFacet, + _Lazy> aliases, + _Lazy isDomainServiceLazy, + _Lazy isInjectableLazy) implements - HasLogicalType, - HasFacetHolder, - Specification, - ObjectActionContainer, - ObjectAssociationContainer, - Hierarchical - //ObjectSpecification -{ + ObjectSpecification { + +// ObjectSpecificationRecord( +// final CausewayBeanMetaData typeMeta, +// final FeatureType featureType, +// final FacetHolder facetHolder, +// final Hierarchical hierarchical, +// final ObjectActionContainer actionContainer, +// final ObjectAssociationContainer associationContainer, +// final IntrospectionPolicy introspectionPolicy) { +// this(typeMeta, featureType, facetHolder, hierarchical, actionContainer, associationContainer, introspectionPolicy, +// null, null, null, null, +// _Lazy.threadSafe(()->Hierarchical.lookupFacet(AliasedFacet.class, facetHolder, hierarchical) +// .map(AliasedFacet::getAliases) +// .orElseGet(Can::empty)), +// _Lazy.threadSafe(()->_ClassCache.getInstance() +// .head(typeMeta.getCorrespondingClass()) +// .hasAnnotation(DomainService.class)) +// ); +// } + + public ObjectSpecificationRecord { + aliases = _Lazy.threadSafe(()->Hierarchical.lookupFacet(AliasedFacet.class, facetHolder, hierarchical) + .map(AliasedFacet::getAliases) + .orElseGet(Can::empty)); + isDomainServiceLazy = _Lazy.threadSafe(()->_ClassCache.getInstance() + .head(typeMeta.getCorrespondingClass()) + .hasAnnotation(DomainService.class)); + + boolean isVetoedForInjection = switch (typeMeta.managedBy()) { + case NONE, CAUSEWAY, PERSISTENCE -> true; + case UNSPECIFIED, SPRING -> false; + }; + + isInjectableLazy = _Lazy.threadSafe(()-> + !isVetoedForInjection + && !typeMeta.beanSort().isAbstract() + && !typeMeta.beanSort().isValue() + && !typeMeta.beanSort().isEntity() + && !typeMeta.beanSort().isViewModel() + && !typeMeta.beanSort().isMixin() + && (typeMeta.beanSort().isManagedBeanAny() + || getServiceRegistry() + .lookupRegisteredBeanById(typeMeta.logicalType()) + .isPresent())); + } + // -- SPECIFICATION @@ -113,4 +186,124 @@ 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(), getBeanSort().name(), superclass() == null + ? "Object" + : superclass().getFullIdentifier()); + } + + // -- COMPONENTS AND GETTERS + + @Override public BeanSort getBeanSort() { 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 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); } + + @Override + public Optional getMember(final String memberId) { + // TODO Auto-generated method stub + return Optional.empty(); + } + @Override + public Optional getMember(final ResolvedMethod method) { + // TODO Auto-generated method stub + return Optional.empty(); + } + @Override + public String getSingularName() { + // TODO Auto-generated method stub + return null; + } + @Override + public String getDescription() { + // TODO Auto-generated method stub + return null; + } + @Override + public String getHelp() { + // TODO Auto-generated method stub + return null; + } + @Override + public String getTitle(final TitleRenderRequest titleRenderRequest) { + // TODO Auto-generated method stub + return null; + } + @Override + public Optional getIcon(final ManagedObject object, final IconSize iconSize) { + // TODO Auto-generated method stub + return Optional.empty(); + } + @Override + public Object getNavigableParent(final Object object) { + // TODO Auto-generated method stub + return null; + } + @Override + public String getCssClass(final ManagedObject domainObject) { + // TODO Auto-generated method stub + return null; + } + @Override + public Optional explicitElementSpec() { + // TODO Auto-generated method stub + return Optional.empty(); + } + @Override + public Optional contributing() { + // TODO Auto-generated method stub + return Optional.empty(); + } + @Override + public ObjectTitleContext createTitleInteractionContext(final ManagedObject targetObjectAdapter, + final InteractionInitiatedBy invocationMethod) { + // TODO Auto-generated method stub + return null; + } + @Override + public ObjectValidityContext createValidityInteractionContext(final ManagedObject targetAdapter, + final InteractionInitiatedBy interactionInitiatedBy) { + // TODO Auto-generated method stub + return null; + } + @Override + public Consent isValid(final ManagedObject targetAdapter, final InteractionInitiatedBy interactionInitiatedBy) { + // TODO Auto-generated method stub + return null; + } + @Override + public InteractionResult isValidResult(final ManagedObject targetAdapter, final InteractionInitiatedBy interactionInitiatedBy) { + // TODO Auto-generated method stub + return null; + } + + // -- FACET LOOKUP + + @Override + public Optional lookupFacet(final Class facetType) { + return Hierarchical.lookupFacet(facetType, facetHolder, this); + } + } 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/FacetedMethodsBuilder.java index 3b584473d03..80b37faf4a4 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/FacetedMethodsBuilder.java @@ -54,6 +54,7 @@ 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.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionRequest; import org.apache.causeway.core.metamodel.specloader.typeextract.TypeExtractor; @@ -104,7 +105,7 @@ public Can snapshotMethodsRemaining() { } - private final ObjectSpecificationDefault inspectedTypeSpec; + private final ObjectSpecification inspectedTypeSpec; @Getter private final Class introspectedClass; @@ -120,7 +121,7 @@ public Can snapshotMethodsRemaining() { // -- CONSTRUCTOR public FacetedMethodsBuilder( - final ObjectSpecificationDefault inspectedTypeSpec, + final ObjectSpecification inspectedTypeSpec, final FacetProcessor facetProcessor, final ClassSubstitutorRegistry classSubstitutorRegistry) { @@ -399,11 +400,14 @@ private boolean representsAction(final ResolvedMethod actionMethod) { return true; } - // exclude those that have eg. reserved prefixes - if (getFacetProcessor().recognizes(actionMethod)) { - // this is a potential orphan candidate, collect these, than use when validating - inspectedTypeSpec.getPotentialOrphans().add(actionMethod); - return false; + //FIXME potentially misses other ObjectSpecification impl. + if(inspectedTypeSpec instanceof ObjectSpecificationDefault objspecDefault) { + // exclude those that have eg. reserved prefixes + if (getFacetProcessor().recognizes(actionMethod)) { + // this is a potential orphan candidate, collect these, than use when validating + objspecDefault.getPotentialOrphans().add(actionMethod); + return false; + } } if(introspectionPolicy().getMemberAnnotationPolicy().isMemberAnnotationsRequired()) { @@ -434,9 +438,12 @@ private boolean isMixinMain(final ResolvedMethod method) { .orElse(null); if(mixinFacet==null) return false; - if(!inspectedTypeSpec.isFullyIntrospected()) - // members are not introspected yet, so make a guess - return mixinFacet.isCandidateForMain(method); + //FIXME potentially misses other ObjectSpecification impl. + if(inspectedTypeSpec instanceof ObjectSpecificationDefault objspecDefault) { + if(!objspecDefault.isFullyIntrospected()) + // members are not introspected yet, so make a guess + return mixinFacet.isCandidateForMain(method); + } return inspectedTypeSpec .lookupMixedInAction(inspectedTypeSpec) diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator.java new file mode 100644 index 00000000000..63ca75ce56d --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator.java @@ -0,0 +1,85 @@ +/* + * 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.applib.id.LogicalType; +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; + +interface MemberPopulator { + + 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(); + } + } + + record ComputedMembers( + Can associationsInOrder, + Can actionsInOrder + //Map membersByMethod, + ) { + + ComputedMembers() { + this(Can.empty(), Can.empty()); + } + + ComputedMembers( + final Stream associations, + final Stream actions) { + this( + Can.ofCollection(_MemberSortingUtils.sortAssociationsIntoList(associations)), + Can.ofCollection(_MemberSortingUtils.sortActionsIntoList(actions))); + } + + ComputedMembers join(final ComputedMembers other) { + return new ComputedMembers( + Stream.concat(this.associationsInOrder.stream(), other.associationsInOrder.stream()), + Stream.concat(this.actionsInOrder.stream(), other.actionsInOrder.stream())); + } + + } + +} \ No newline at end of file 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..fd3e35691fc --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixedInMemberFactory.java @@ -0,0 +1,126 @@ +/* + * 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.stream.Stream; + +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.ObjectAssociation; +import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionRequest; + +record MixedInMemberFactory( + //FIXME refactor to use ObjectSpecification + ObjectSpecificationDefault spec) { + + /** + * Creates all mixed in properties and collections for this spec. + */ + public List createMixedInAssociations() { + var include = spec.isEntityOrViewModelOrAbstract() + && !spec.isInjectable() + && !spec.isValue(); + return include + ? spec.getCausewayBeanTypeRegistry().streamMixinTypes() + .flatMap(this::createMixedInAssociation) + .toList() + : List.of(); + } + + /** + * Creates all mixed in actions for this spec. + */ + public List createMixedInActions() { + var include = spec.isEntityOrViewModelOrAbstract() + || spec.getBeanSort().isManagedBeanContributing() + // in support of composite value-type constructor mixins + || spec.getBeanSort().isValue(); + return include + ? spec.getCausewayBeanTypeRegistry().streamMixinTypes() + .flatMap(this::createMixedInAction) + .toList() + : List.of(); + } + + // -- HELPER + + private Stream createMixedInAssociation(final Class mixinType) { + var mixinSpec = spec.specLoaderInternal().loadSpecification(mixinType, + IntrospectionRequest.FULL); + if (mixinSpec == null + || mixinSpec == spec) + 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(spec.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(spec, mixinSpec, mixinMethodName)); + } + + private Stream createMixedInAction(final Class mixinType) { + + var mixinSpec = spec.specLoaderInternal().loadSpecification(mixinType, + IntrospectionRequest.FULL); + if (mixinSpec == null + || mixinSpec == spec) + 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(spec.getCorrespondingClass())) + return Stream.empty(); + // don't mixin Object_ mixins to domain services + if(spec.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(spec, mixinSpec, mixinMethodName)); + } + + /** + * 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.getBeanSort().isValue() + ? Objects.equals(spec, act.getReturnType()) + : true; + } + +} 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..283f18a89aa 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,6 +18,8 @@ */ package org.apache.causeway.core.metamodel.spec.impl; +import static org.apache.causeway.commons.internal.base._NullSafe.stream; + import java.lang.reflect.Method; import java.util.List; import java.util.Map; @@ -25,18 +27,10 @@ import java.util.Optional; import java.util.Set; import java.util.function.BiConsumer; -import java.util.function.Predicate; 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; @@ -48,9 +42,7 @@ 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; @@ -58,7 +50,6 @@ 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.reflection._ClassCache; import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; import org.apache.causeway.commons.internal.reflection._MethodFacades.MethodFacade; @@ -72,7 +63,6 @@ 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; @@ -93,7 +83,6 @@ 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; @@ -108,18 +97,22 @@ 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.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.spec.impl.MemberPopulator.IntrospectionState; 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.jspecify.annotations.Nullable; +import org.springframework.util.ClassUtils; import lombok.Getter; +import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -136,9 +129,13 @@ final class ObjectSpecificationDefault private final FacetedMethodsBuilder facetedMethodsBuilder; private final ClassSubstitutorRegistry classSubstitutorRegistry; private final _MembersAsColumns columnHelper; + private final _Lazy isInjectableLazy; @Getter(onMethod_={@Override}) private final IntrospectionPolicy introspectionPolicy; + + @Getter @Accessors(fluent = true) + private final CausewayBeanMetaData typeMeta; public ObjectSpecificationDefault( final @NonNull CausewayBeanMetaData typeMeta, @@ -147,23 +144,14 @@ public ObjectSpecificationDefault( final @NonNull PostProcessor postProcessor, final @NonNull ClassSubstitutorRegistry classSubstitutorRegistry) { - this.correspondingClass = typeMeta.getCorrespondingClass(); - this.logicalType = typeMeta.logicalType(); - this.fullName = correspondingClass.getName(); - this.shortName = typeMeta.logicalType().logicalSimpleName(); - this.beanSort = typeMeta.beanSort(); + this.typeMeta = typeMeta; + this.isInjectableLazy = _Lazy.threadSafe(()->typeMeta.isInjectable(getServiceRegistry())); this.facetHolder = FacetHolder.simple( facetProcessor.getMetaModelContext(), - Identifier.classIdentifier(logicalType)); + 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) @@ -179,17 +167,30 @@ public ObjectSpecificationDefault( this.columnHelper = new _MembersAsColumns(mmc); } + + @Override public BeanSort getBeanSort() { 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 @@ -231,9 +232,11 @@ private void introspectMembers() { return; } + var memberFactory = new RegularMemberFactory(this, facetedMethodsBuilder); + // create associations and actions - replaceAssociations(createAssociations()); - replaceActions(createActions()); + replaceAssociations(memberFactory.createAssociations()); + replaceActions(memberFactory.createActions()); postProcessor.postProcess(this); invalidateCachedFacets(); @@ -278,47 +281,6 @@ 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 @@ -405,23 +367,6 @@ 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(); @@ -442,9 +387,6 @@ public boolean isDomainService() { // -- FIELDS private final PostProcessor postProcessor; - private final FacetProcessor facetProcessor; - - @Getter private final BeanSort beanSort; // -- ASSOCIATIONS @@ -477,16 +419,6 @@ public boolean isDomainService() { 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; @@ -509,30 +441,6 @@ 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) { @@ -545,29 +453,6 @@ public void introspect(final IntrospectionRequest 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 */ @@ -891,50 +776,12 @@ public final Optional contributing() { } // -- FACET HANDLING - + @Override public Q getFacet(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); - - } - } - - @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; + return Hierarchical.lookupFacet(facetType, facetHolder, this) + .orElse(null); } } @@ -1027,87 +874,6 @@ public Stream streamDeclaredActions( .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; - } // -- VALIDITY @@ -1164,14 +930,8 @@ public boolean isParented() { * 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()); + var memberFactory = new MixedInMemberFactory(this); + var mixedInActions = memberFactory.createMixedInActions(); if(mixedInActions.isEmpty()) return; // nothing to do (this spec has no mixed-in actions, regular actions have already been added) @@ -1189,10 +949,8 @@ private void createMixedInActionsAndResort() { * one-shot: must be no-op, if already created */ private void createMixedInAssociationsAndResort() { - if(!isEntityOrViewModelOrAbstract()) - return; - var mixedInAssociations = createMixedInAssociations() - .collect(Collectors.toList()); + var memberFactory = new MixedInMemberFactory(this); + var mixedInAssociations = memberFactory.createMixedInAssociations(); if(mixedInAssociations.isEmpty()) return; // nothing to do (this spec has no mixed-in associations, regular associations have already been added) 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..d67564f9fc1 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/RegularMemberFactory.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.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.ObjectSpecification; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; + +record RegularMemberFactory( + ObjectSpecification spec, + FacetedMethodsBuilder facetedMethodsBuilder) { + + // -- create associations and actions + Stream createAssociations() { + return facetedMethodsBuilder.getAssociationFacetedMethods() + .stream() + .map(this::createAssociation) + .filter(_NullSafe::isPresent); + } + + Stream createActions() { + return facetedMethodsBuilder.getActionFacetedMethods() + .stream() + .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/SpecificationLoaderInternal.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderInternal.java index 47235a2515e..5f35b96adba 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 @@ -61,9 +61,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)) 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..8d50e3a43ff 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,6 +18,7 @@ */ package org.apache.causeway.core.metamodel.spec.impl; +import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; @@ -36,7 +37,7 @@ final class _MemberSortingUtils { static List sortAssociationsIntoList(final Stream associations) { var deweyOrderSet = DeweyOrderSet.createOrderSet(associations); - var orderedAssociations = _Lists. newArrayList(); + var orderedAssociations = new ArrayList(); sortAssociations(deweyOrderSet, orderedAssociations); return orderedAssociations; } @@ -45,7 +46,7 @@ static List sortAssociationsIntoList(final Stream sortActionsIntoList(final Stream actions) { var deweyOrderSet = DeweyOrderSet.createOrderSet(actions); - var orderedActions = _Lists.newArrayList(); + var orderedActions = new ArrayList(); sortActions(deweyOrderSet, orderedActions); return orderedActions; } @@ -58,30 +59,25 @@ private static void sortAssociations(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/wip/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator2.java b/core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator2.java new file mode 100644 index 00000000000..f2034775aa4 --- /dev/null +++ b/core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator2.java @@ -0,0 +1,379 @@ +/* + * 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.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.commons.internal.base._Casts; +import org.apache.causeway.commons.internal.base._NullSafe; +import org.apache.causeway.commons.internal.base._Oneshot; +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.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.ObjectAssociation; +import org.apache.causeway.core.metamodel.spec.impl.MemberPopulator.ComputedMembers; +import org.apache.causeway.core.metamodel.spec.impl.MemberPopulator.IntrospectionState; +import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionRequest; +import org.apache.causeway.core.metamodel.util.Facets; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RequiredArgsConstructor +@Slf4j +final class MemberPopulator2 { + private final ObjectSpecification2 spec; + private IntrospectionState introspectionState = IntrospectionState.NOT_INTROSPECTED; + private final _Oneshot mixedInMembersAdder = new _Oneshot(); + private ComputedMembers computedMembers = new ComputedMembers(); + + 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)); + } + } + + boolean isFullyIntrospected() { + return introspectionState == IntrospectionState.FULLY_INTROSPECTED; + } + + void includeMixedInMembers(final Supplier introspectionContextProvider) { + introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, introspectionContextProvider); + mixedInMembersAdder.trigger(()->{ + var associationsInOrder = createMixedInAssociationsAndResort(computedMembers.associationsInOrder().toList()); + var actionsInOrder = createMixedInActionsAndResort(computedMembers.actionsInOrder().toList()); + + spec.replaceMembers(new ComputedMembers(Can.ofCollection(associationsInOrder), Can.ofCollection(actionsInOrder))); + }); + } + + /** + * @param introspectionContextProvider keeps track of the causal chain of introspection requests + */ + void introspectUpTo(final IntrospectionState upTo, final Supplier introspectionContextProvider) { + if(!isLessThan(upTo)) + return; // optimization + + if(log.isDebugEnabled()) { + log.debug("introspectingUpTo: {}, {}", spec.getFullIdentifier(), upTo); + } + + switch (introspectionState) { + case NOT_INTROSPECTED->{ + if(isLessThan(upTo)) { + introspectType(); + } + if(isLessThan(upTo)) { + introspectFully(); + spec.specLoaderInternal().validateLater(spec, introspectionContextProvider); + } + } + case TYPE_BEING_INTROSPECTED->{} // nothing to do (interim state during introspectType) + case TYPE_INTROSPECTED->{ + if(isLessThan(upTo)) { + introspectFully(); + spec.specLoaderInternal().validateLater(spec, introspectionContextProvider); + } + } + case MEMBERS_BEING_INTROSPECTED->{}// nothing to do (interim state during introspect fully) + case FULLY_INTROSPECTED->{}// nothing to do ... all done + } + } + + private boolean isLessThan(final IntrospectionState upTo) { + return introspectionState.isLessThan(upTo); + } + + private void introspectType() { + // set to avoid infinite loops + this.introspectionState = IntrospectionState.TYPE_BEING_INTROSPECTED; + introspectTypeHierarchy(); + spec.invalidateCachedFacets(); + this.introspectionState = IntrospectionState.TYPE_INTROSPECTED; + } + + private void introspectTypeHierarchy() { + spec.facetedMethodsBuilder.introspectClass(); + + // name + spec.addNamedFacetIfRequired(); + + // go no further if a value + if(spec.isValue()) { + if (log.isDebugEnabled()) { + log.debug("skipping type hierarchy introspection for value type {}", spec.getFullIdentifier()); + } + return; + } + + spec.loadSpecOfSuperclass(spec.getCorrespondingClass().getSuperclass()); + spec.loadSpecOfInterfaces(spec.getCorrespondingClass().getInterfaces()); + } + + private void introspectMembers() { + + // yet this logic does not skip UNKNONW + if(spec.getBeanSort().isCollection() + || spec.getBeanSort().isVetoed() + || spec.isValue()) { + if (log.isDebugEnabled()) { + log.debug("skipping full introspection for {} type {}", spec.getBeanSort(), spec.getFullIdentifier()); + } + return; + } + + // create associations and actions + this.computedMembers = new ComputedMembers( + Stream.concat( + createAssociations(spec.facetedMethodsBuilder), + createMixedInAssociations()), + Stream.concat( + createActions(spec.facetedMethodsBuilder), + createMixedInActions())); + + spec.replaceMembers(computedMembers); + + spec.postProcessor.postProcess(spec); + spec.invalidateCachedFacets(); + } + + 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(spec, null); + } + + // -- ASSOC CREATION + + private Stream createAssociations(final FacetedMethodsBuilder facetedMethodsBuilder) { + 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; + } + + // -- ACTION CREATION + + private Stream createActions(final FacetedMethodsBuilder facetedMethodsBuilder) { + 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. */ + 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; + } + + // -- MIXED IN MEMBERS + + /** + * Creates all mixed in properties and collections for this spec. + */ + private Stream createMixedInAssociations() { + if(true) return Stream.empty(); //FIXME + if (spec.isInjectable() || spec.isValue()) + return Stream.empty(); + return spec.getCausewayBeanTypeRegistry().streamMixinTypes() + .flatMap(this::createMixedInAssociation); + } + + private Stream createMixedInAssociation(final Class mixinType) { + var mixinSpec = spec.specLoaderInternal().loadSpecification(mixinType, + IntrospectionRequest.FULL); + if (mixinSpec == null + || mixinSpec == spec) + 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(spec.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(spec, mixinSpec, mixinMethodName)); + } + + // -- mixin actions + /** + * Creates all mixed in actions for this spec. + */ + private Stream createMixedInActions() { + if(true) return Stream.empty(); //FIXME + return spec.getCausewayBeanTypeRegistry().streamMixinTypes() + .flatMap(this::createMixedInAction); + } + + private Stream createMixedInAction(final Class mixinType) { + var mixinSpec = spec.specLoaderInternal().loadSpecification(mixinType, + IntrospectionRequest.FULL); + if (mixinSpec == null + || mixinSpec == spec) + 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(spec.getCorrespondingClass())) + return Stream.empty(); + // don't mixin Object_ mixins to domain services + if(spec.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(spec, mixinSpec, mixinMethodName)); + } + + /** + * 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.getBeanSort().isValue() + ? Objects.equals(spec, act.getReturnType()) + : true; + } + + // -- + + /** + * one-shot: must be no-op, if already created + * @return + */ + private List createMixedInActionsAndResort(final List regularActions) { + var include = spec.isEntityOrViewModelOrAbstract() + || spec.getBeanSort().isManagedBeanContributing() + // in support of composite value-type constructor mixins + || spec.getBeanSort().isValue(); + if(!include) + return regularActions; + var mixedInActions = createMixedInActions() + .collect(Collectors.toList()); + if(mixedInActions.isEmpty()) + return regularActions; // nothing to do (this spec has no mixed-in actions, regular actions have already been added) + + // note: we are doing this before any member sorting + _MemberIdClashReporting.flagAnyMemberIdClashes(spec, regularActions, mixedInActions); + + return _MemberSortingUtils.sortActionsIntoList(Stream.concat( + regularActions.stream(), + mixedInActions.stream())); + } + + /** + * one-shot: must be no-op, if already created + */ + private List createMixedInAssociationsAndResort(final List regularAssociations) { + if(!spec.isEntityOrViewModelOrAbstract()) + return regularAssociations; + var mixedInAssociations = createMixedInAssociations() + .collect(Collectors.toList()); + if(mixedInAssociations.isEmpty()) + return regularAssociations; // nothing to do (this spec has no mixed-in associations, regular associations have already been added) + + // note: we are doing this before any member sorting + _MemberIdClashReporting.flagAnyMemberIdClashes(spec, regularAssociations, mixedInAssociations); + + return _MemberSortingUtils.sortAssociationsIntoList(Stream.concat( + regularAssociations.stream(), + mixedInAssociations.stream())); + } + + +// 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))); +// } + +} \ No newline at end of file diff --git a/core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecification2.java b/core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecification2.java new file mode 100644 index 00000000000..39ea2ba1df1 --- /dev/null +++ b/core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecification2.java @@ -0,0 +1,821 @@ +/* + * 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 static org.apache.causeway.commons.internal.base._NullSafe.stream; + +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.stream.Stream; + +import org.apache.causeway.applib.Identifier; +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._Lazy; +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.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.ImperativeFacet; +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; +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.immutable.ImmutableFacet; +import org.apache.causeway.core.metamodel.facets.object.introspection.IntrospectionPolicyFacet; +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.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.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.spec.impl.MemberPopulator.ComputedMembers; +import org.apache.causeway.core.metamodel.spec.impl.MemberPopulator.IntrospectionState; +import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure; +import org.apache.causeway.core.metamodel.spi.EntityTitleSubscriber; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.springframework.util.ClassUtils; + +import lombok.Getter; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +final class ObjectSpecification2 +implements ObjectMemberContainer, ObjectSpecificationMutable, HasSpecificationLoaderInternal { + + // -- CONSTRUCTION + + /** + * Lazily built by {@link #getMember(Method)}. + */ + private Map membersByMethod = null; + + final FacetedMethodsBuilder facetedMethodsBuilder; + private final ClassSubstitutorRegistry classSubstitutorRegistry; + private final _MembersAsColumns columnHelper; + private final _Lazy isInjectableLazy; + + @Getter(onMethod_={@Override}) + private final IntrospectionPolicy introspectionPolicy; + + @Getter @Accessors(fluent = true) + private final CausewayBeanMetaData typeMeta; + + public ObjectSpecification2( + final @NonNull CausewayBeanMetaData typeMeta, + final @NonNull MetaModelContext mmc, + final @NonNull FacetProcessor facetProcessor, + final @NonNull PostProcessor postProcessor, + final @NonNull ClassSubstitutorRegistry classSubstitutorRegistry) { + + this.typeMeta = typeMeta; + this.isInjectableLazy = _Lazy.threadSafe(()->typeMeta.isInjectable(getServiceRegistry())); + + this.facetHolder = FacetHolder.simple( + facetProcessor.getMetaModelContext(), + Identifier.classIdentifier(logicalType())); + + this.facetProcessor = facetProcessor; + this.postProcessor = postProcessor; + + 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) + .map(IntrospectionPolicyFacet::getIntrospectionPolicy) + .orElseGet(()->mmc.getConfiguration().core().metaModel().introspector().policy()); + + this.facetedMethodsBuilder = + new FacetedMethodsBuilder(this, facetProcessor, classSubstitutorRegistry); + + this.columnHelper = new _MembersAsColumns(mmc); + this.memberPopulator = new MemberPopulator2(this); + } + + @Override public BeanSort getBeanSort() { 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 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(), getBeanSort().name(), superclass() == null + ? "Object" + : superclass().getFullIdentifier()); + } + + void addNamedFacetIfRequired() { + if (getFacet(MemberNamedFacet.class) == null) { + addFacet(new MemberNamedFacetForStaticMemberName( + _Strings.asNaturalName.apply(getShortIdentifier()), + this)); + } + } + + // -- getObjectAction + + @Override + public Optional getDeclaredAction( + final @Nullable String id, + final ImmutableEnumSet actionScopes, + final MixedIn mixedIn) { + + memberPopulator.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) { + memberPopulator.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))); + } + + // -- 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); + } + + @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 + + final PostProcessor postProcessor; + final FacetProcessor facetProcessor; + + // -- 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 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 final MemberPopulator2 memberPopulator; + + @Getter(onMethod_ = {@Override}) private FacetHolder facetHolder; + + // -- Stuff immediately derivable from class + @Override + public final FeatureType getFeatureType() { + return FeatureType.OBJECT; + } + + @Override + public void introspect(final IntrospectionRequest request) { + memberPopulator.introspect(request); + } + + protected void loadSpecOfSuperclass(final Class superclass) { + if (superclass == null) + return; + + this.superclassSpec = specLoaderInternal().loadSpecification(superclass); + if (superclassSpec != null + && log.isDebugEnabled()) { + log.debug(" Superclass {}", superclass.getName()); + } + } + + protected void loadSpecOfInterfaces(final Class[] interfaces) { + if(interfaces==null) + return; + + var classCache = _ClassCache.getInstance(); + + final List interfaceSpecList = Stream.of(interfaces) + // pre-filter common interfaces (performance) + .filter(interfaceType->!interfaceType.getName().startsWith("java.")) + //-- + .map(interfaceType->{ + var substitution = classSubstitutorRegistry.getSubstitution(interfaceType); + return substitution.isReplace() + ? substitution.replacement() + : substitution.isNeverIntrospect() + ? null + : interfaceType; + }) + .filter(Objects::nonNull) + .filter(interfaceType->classCache.head(interfaceType).hasAnnotation(DomainObject.class)) + .map(specLoaderInternal()::loadSpecification) + .filter(Objects::nonNull) + .toList(); + + if(!interfaceSpecList.isEmpty()) { + if(interfaceSpecList.size()>1) { + ValidationFailure.raiseFormatted(facetHolder, + "Cannot use @DomainObject on more than one interface, as inherited by: %s", + getCorrespondingClass().getName()); + } + if (superclassSpec != null) { + var superType = superclassSpec.getCorrespondingClass(); + if(classCache.head(superType).hasAnnotation(DomainObject.class)) { + ValidationFailure.raiseFormatted(facetHolder, + "Cannot use @DomainObject on both, abstract super class and one interface, as inherited by: %s", + getCorrespondingClass().getName()); + } + } + +//debug +// System.err.println("%s".formatted(getCorrespondingClass().getName())); +// interfaceSpecList.forEach(i->{ +// System.err.println("- %s".formatted(i.getCorrespondingClass().getName())); +// }); + synchronized(unmodifiableInterfaces) { + this.interfaces.clear(); + this.interfaces.addAll(interfaceSpecList); + unmodifiableInterfaces.clear(); + } + } + } + + void invalidateCachedFacets() { + this.valueFacet = getFacet(ValueFacet.class); + this.titleFacet = lookupNonFallbackFacet(TitleFacet.class).orElse(null); + this.iconFacet = getFacet(IconFacet.class); + this.navigableParentFacet = getFacet(NavigableParentFacet.class); + this.cssClassFacet = getFacet(CssClassFacet.class); + this.aliasedFacet = getFacet(AliasedFacet.class); + } + + @Override + public final Optional> valueFacet() { + if(valueFacet == null + && getBeanSort().isValue()) { + invalidateCachedFacets(); + } + return Optional.ofNullable(valueFacet); + } + + @Override + public final Optional mixinFacet() { + // deliberately don't memoize lookup misses, because could be too early + if(mixinFacet==null) { + mixinFacet = getFacet(MixinFacet.class); + } + return Optional.ofNullable(mixinFacet); + } + + @Override + public final Optional entityFacet() { + // deliberately don't memoize lookup misses, because could be too early + if(entityFacet==null) { + entityFacet = getFacet(EntityFacet.class); + } + return Optional.ofNullable(entityFacet); + } + + @Override + public final Optional viewmodelFacet() { + // deliberately don't memoize lookup misses, because could be too early + if(viewmodelFacet==null) { + viewmodelFacet = getFacet(ViewModelFacet.class); + } + return Optional.ofNullable(viewmodelFacet); + } + + @Override + public String getTitle(final TitleRenderRequest titleRenderRequest) { + if (titleFacet != null) { + var titleString = titleFacet.title(titleRenderRequest); + if (!_Strings.isEmpty(titleString)) { + notifySubscribersIfEntity(titleRenderRequest, titleString); + return titleString; + } + } + var prefix = this.isInjectable() + ? "" + : "Untitled "; + 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 + ? navigableParentFacet.navigableParent(object) + : null; + } + + @Override + public String getCssClass(final ManagedObject reference) { + return cssClassFacet != null + ? cssClassFacet.cssClass(reference) + : null; + } + + @Override + public Can getAliases() { + return aliasedFacet != null + ? aliasedFacet.getAliases() + : Can.empty(); + } + + // -- ICON + + @Override + public Optional getIcon(final ManagedObject domainObject, final ObjectSupport.IconSize iconSize) { + if(ManagedObjects.isSpecified(domainObject)) { + _Assert.assertEquals(domainObject.objSpec(), this); + } + return Optional.ofNullable(iconFacet) + .flatMap(facet->facet.icon(domainObject, iconSize)) + .or(()->faLayers(domainObject) + .map(ObjectSupport.FontAwesomeIconResource::new)); + } + + private Optional faLayers(final ManagedObject domainObject){ + return lookupFacet(FaFacet.class) + .map(FaFacet::getSpecialization) + .map(either->either.fold( + faStaticFacet->(FaLayersProvider)faStaticFacet, + faImperativeFacet->faImperativeFacet.getFaLayersProvider(domainObject))) + .map(FaLayersProvider::getLayers); + } + + // -- HIERARCHICAL + + @Override + public boolean isOfType(final ObjectSpecification other) { + + var thisClass = this.getCorrespondingClass(); + var otherClass = other.getCorrespondingClass(); + + return thisClass == otherClass + || otherClass.isAssignableFrom(thisClass); + } + + @Override + public boolean isOfTypeResolvePrimitive(final ObjectSpecification other) { + + var thisClass = ClassUtils.resolvePrimitiveIfNecessary(this.getCorrespondingClass()); + var otherClass = ClassUtils.resolvePrimitiveIfNecessary(other.getCorrespondingClass()); + + return thisClass == otherClass + || otherClass.isAssignableFrom(thisClass); + } + + // -- NAME, DESCRIPTION, PERSISTABILITY + + @Override + public String getSingularName() { + return lookupFacet(ObjectNamedFacet.class) + .flatMap(ObjectNamedFacet::translated) + // unexpected code reach, however keep for JUnit testing + .orElseGet(()->String.format( + "(%s has neither title- nor object-named-facet)", + getFullIdentifier())); + } + + /** + * The translated description according to any available {@link ObjectDescribedFacet}, + * else empty string (""). + */ + @Override + public String getDescription() { + return lookupFacet(ObjectDescribedFacet.class) + .map(ObjectDescribedFacet::translated) + .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() + .map(MixinFacet::contributing); + } + + // -- FACET HANDLING + + @Override + public Q getFacet(final Class facetType) { + synchronized(unmodifiableInterfaces) { + return Hierarchical.lookupFacet(facetType, facetHolder, this) + .orElse(null); + } + } + + @Override + public ObjectTitleContext createTitleInteractionContext( + final ManagedObject targetObjectAdapter, + final InteractionInitiatedBy interactionMethod) { + + return new ObjectTitleContext(targetObjectAdapter, getFeatureIdentifier(), + targetObjectAdapter.getTitle(), + interactionMethod); + } + + // -- INHERITED + + @Override + public ObjectSpecification superclass() { + return superclassSpec; + } + + @Override + public Can interfaces() { + return unmodifiableInterfaces.get(); + } + + // -- ASSOCIATIONS + + @Override + public Stream streamDeclaredAssociations(final MixedIn mixedIn) { + memberPopulator.includeMixedInMembers( + ()->"streamDeclaredAssociations of %s".formatted(this.getFeatureIdentifier())); + + synchronized(unmodifiableAssociations) { + return stream(unmodifiableAssociations.get()) + .filter(mixedIn.toFilter()); + } + } + + @Override + public Optional getMember(final String memberId) { + memberPopulator.introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, + ()->"getMember %s of %s".formatted(memberId, this.getFeatureIdentifier())); + + 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 getDeclaredAssociation(final String id, final MixedIn mixedIn) { + memberPopulator.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) { + memberPopulator.includeMixedInMembers( + ()->"streamDeclaredActions of %s".formatted(this.getFeatureIdentifier())); + + return actionScopes.stream() + .flatMap(actionScope->stream(objectActionsByType.get(actionScope))) + .filter(mixedIn.toFilter()); + } + + // -- 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); + } + + @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 memberPopulator.isFullyIntrospected(); + } + + @Deprecated + void replaceMembers(final ComputedMembers computedMembers) { + synchronized (unmodifiableAssociations) { + associations.clear(); + associations.addAll(computedMembers.associationsInOrder().toList()); + unmodifiableAssociations.clear(); // invalidate + } + synchronized (unmodifiableActions) { + objectActions.clear(); + objectActions.addAll(computedMembers.actionsInOrder().toList()); + unmodifiableActions.clear(); // invalidate + // rebuild objectActionsByType multi-map + for (var actionType : ActionScope.values()) { + var objectActionForType = objectActionsByType.getOrElseNew(actionType); + objectActionForType.clear(); + computedMembers.actionsInOrder().stream() + .filter(ObjectAction.Predicates.ofActionType(actionType)) + .forEach(objectActionForType::add); + } + } + } + +// @Deprecated +// protected final void replaceAssociations(final Stream associations) { +// var orderedAssociations = _MemberSortingUtils.sortAssociationsIntoList(associations); +// synchronized (unmodifiableAssociations) { +// this.associations.clear(); +// this.associations.addAll(orderedAssociations); +// unmodifiableAssociations.clear(); // invalidate +// } +// } +// +// @Deprecated +// 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); +// } +// } +// } + +} 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..6a5eb3deedb 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.MemberPopulator.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/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/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/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..a83a73eefab 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 @@ -26,6 +26,7 @@ import jakarta.inject.Inject; 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; @@ -111,6 +112,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; @@ -1006,6 +1008,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 From 953ea5bafbc7e1fcf2a586bb9b2c49dfca8d0426 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Wed, 29 Jul 2026 07:11:08 +0200 Subject: [PATCH 02/22] CAUSEWAY-4044: consolidate mixed-in member factory code Task-Url: https://issues.apache.org/jira/browse/CAUSEWAY-4044 --- .../spec/impl/MixedInMemberFactory.java | 51 +- .../spec/impl/ObjectSpecificationDefault.java | 33 +- .../spec/impl/RegularMemberFactory.java | 7 +- .../spec/impl/_MixedInMemberFactory.java | 56 -- .../metamodel/spec/impl/MemberPopulator2.java | 379 -------- .../spec/impl/ObjectSpecification2.java | 821 ------------------ 6 files changed, 59 insertions(+), 1288 deletions(-) delete mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/_MixedInMemberFactory.java delete mode 100644 core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator2.java delete mode 100644 core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecification2.java 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 index fd3e35691fc..3e90906811f 100644 --- 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 @@ -20,18 +20,29 @@ import java.util.List; import java.util.Objects; +import java.util.function.Function; import java.util.stream.Stream; +import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry; 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; import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionRequest; record MixedInMemberFactory( - //FIXME refactor to use ObjectSpecification - ObjectSpecificationDefault spec) { - + ObjectSpecification spec, + SpecificationLoaderInternal specLoaderInternal, + CausewayBeanTypeRegistry causewayBeanTypeRegistry) { + + MixedInMemberFactory( + ObjectSpecification spec, + SpecificationLoaderInternal specLoaderInternal) { + this(spec, specLoaderInternal, spec.getServiceRegistry() + .lookupServiceElseFail(CausewayBeanTypeRegistry.class)); + } + /** * Creates all mixed in properties and collections for this spec. */ @@ -40,7 +51,7 @@ public List createMixedInAssociations() { && !spec.isInjectable() && !spec.isValue(); return include - ? spec.getCausewayBeanTypeRegistry().streamMixinTypes() + ? causewayBeanTypeRegistry.streamMixinTypes() .flatMap(this::createMixedInAssociation) .toList() : List.of(); @@ -55,7 +66,7 @@ public List createMixedInActions() { // in support of composite value-type constructor mixins || spec.getBeanSort().isValue(); return include - ? spec.getCausewayBeanTypeRegistry().streamMixinTypes() + ? causewayBeanTypeRegistry.streamMixinTypes() .flatMap(this::createMixedInAction) .toList() : List.of(); @@ -64,7 +75,7 @@ public List createMixedInActions() { // -- HELPER private Stream createMixedInAssociation(final Class mixinType) { - var mixinSpec = spec.specLoaderInternal().loadSpecification(mixinType, + var mixinSpec = specLoaderInternal.loadSpecification(mixinType, IntrospectionRequest.FULL); if (mixinSpec == null || mixinSpec == spec) @@ -80,12 +91,12 @@ private Stream createMixedInAssociation(final Class mixinT return mixinSpec.streamActions(ActionScope.ANY, MixedIn.EXCLUDED) .filter(_SpecPredicates::isMixedInAssociation) .map(ObjectActionDefault.class::cast) - .map(_MixedInMemberFactory.mixedInAssociation(spec, mixinSpec, mixinMethodName)); + .map(mixedInAssociation(spec, mixinSpec, mixinMethodName)); } private Stream createMixedInAction(final Class mixinType) { - var mixinSpec = spec.specLoaderInternal().loadSpecification(mixinType, + var mixinSpec = specLoaderInternal.loadSpecification(mixinType, IntrospectionRequest.FULL); if (mixinSpec == null || mixinSpec == spec) @@ -108,7 +119,7 @@ private Stream createMixedInAction(final Class mixinType .filter(this::whenIsValueThenIsAlsoConstructorMixin) .filter(_SpecPredicates::isMixedInAction) .map(ObjectActionDefault.class::cast) - .map(_MixedInMemberFactory.mixedInAction(spec, mixinSpec, mixinMethodName)); + .map(mixedInAction(spec, mixinSpec, mixinMethodName)); } /** @@ -122,5 +133,27 @@ private boolean whenIsValueThenIsAlsoConstructorMixin(final ObjectAction act) { ? 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/ObjectSpecificationDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java index 283f18a89aa..14aad3b5cc7 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 @@ -21,6 +21,7 @@ import static org.apache.causeway.commons.internal.base._NullSafe.stream; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -55,7 +56,6 @@ 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; @@ -814,7 +814,7 @@ public Stream streamDeclaredAssociations(final MixedIn mixedI introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, ()->"streamDeclaredAssociations of %s".formatted(this.getFeatureIdentifier())); - mixedInAssociationAdder.trigger(this::createMixedInAssociationsAndResort); // only if not already + mixedInMemberAdder.trigger(this::createMixedInMembersAndResort); // only if not already synchronized(unmodifiableAssociations) { return stream(unmodifiableAssociations.get()) @@ -867,7 +867,7 @@ public Stream streamDeclaredActions( introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, ()->"streamDeclaredActions of %s".formatted(this.getFeatureIdentifier())); - mixedInActionAdder.trigger(this::createMixedInActionsAndResort); + mixedInMemberAdder.trigger(this::createMixedInMembersAndResort); return actionScopes.stream() .flatMap(actionScope->stream(objectActionsByType.get(actionScope))) @@ -923,19 +923,23 @@ public boolean isParented() { // -- MIXIN ADDER ONESHOTs - private final _Oneshot mixedInActionAdder = new _Oneshot(); - private final _Oneshot mixedInAssociationAdder = new _Oneshot(); + private final _Oneshot mixedInMemberAdder = new _Oneshot(); /** * one-shot: must be no-op, if already created */ - private void createMixedInActionsAndResort() { - var memberFactory = new MixedInMemberFactory(this); + private void createMixedInMembersAndResort() { + var memberFactory = new MixedInMemberFactory(this, specLoaderInternal()); + createMixedInActionsAndResort(memberFactory); + createMixedInAssociationsAndResort(memberFactory); + } + + private void createMixedInActionsAndResort(MixedInMemberFactory memberFactory) { var mixedInActions = memberFactory.createMixedInActions(); 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 + var regularActions = new ArrayList<>(objectActions); // defensive copy // note: we are doing this before any member sorting _MemberIdClashReporting.flagAnyMemberIdClashes(this, regularActions, mixedInActions); @@ -945,16 +949,12 @@ private void createMixedInActionsAndResort() { mixedInActions.stream())); } - /** - * one-shot: must be no-op, if already created - */ - private void createMixedInAssociationsAndResort() { - var memberFactory = new MixedInMemberFactory(this); + private void createMixedInAssociationsAndResort(MixedInMemberFactory memberFactory) { var mixedInAssociations = memberFactory.createMixedInAssociations(); 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 + var regularAssociations = new ArrayList<>(associations); // defensive copy // note: we are doing this before any member sorting _MemberIdClashReporting.flagAnyMemberIdClashes(this, regularAssociations, mixedInAssociations); @@ -964,11 +964,6 @@ private void createMixedInAssociationsAndResort() { mixedInAssociations.stream())); } - @Getter(lazy = true) - private final CausewayBeanTypeRegistry causewayBeanTypeRegistry = - getServiceRegistry() - .lookupServiceElseFail(CausewayBeanTypeRegistry.class); - @Getter(lazy = true) private final Can titleSubscribers = getServiceRegistry().select(EntityTitleSubscriber.class); 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 index d67564f9fc1..8c0e90affe2 100644 --- 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 @@ -32,7 +32,6 @@ record RegularMemberFactory( ObjectSpecification spec, FacetedMethodsBuilder facetedMethodsBuilder) { - // -- create associations and actions Stream createAssociations() { return facetedMethodsBuilder.getAssociationFacetedMethods() .stream() @@ -42,9 +41,9 @@ Stream createAssociations() { Stream createActions() { return facetedMethodsBuilder.getActionFacetedMethods() - .stream() - .map(this::createAction) - .filter(_NullSafe::isPresent); + .stream() + .map(this::createAction) + .filter(_NullSafe::isPresent); } // -- HELPER 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/wip/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator2.java b/core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator2.java deleted file mode 100644 index f2034775aa4..00000000000 --- a/core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator2.java +++ /dev/null @@ -1,379 +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.List; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.apache.causeway.commons.collections.Can; -import org.apache.causeway.commons.internal.base._Casts; -import org.apache.causeway.commons.internal.base._NullSafe; -import org.apache.causeway.commons.internal.base._Oneshot; -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.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.ObjectAssociation; -import org.apache.causeway.core.metamodel.spec.impl.MemberPopulator.ComputedMembers; -import org.apache.causeway.core.metamodel.spec.impl.MemberPopulator.IntrospectionState; -import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionRequest; -import org.apache.causeway.core.metamodel.util.Facets; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@RequiredArgsConstructor -@Slf4j -final class MemberPopulator2 { - private final ObjectSpecification2 spec; - private IntrospectionState introspectionState = IntrospectionState.NOT_INTROSPECTED; - private final _Oneshot mixedInMembersAdder = new _Oneshot(); - private ComputedMembers computedMembers = new ComputedMembers(); - - 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)); - } - } - - boolean isFullyIntrospected() { - return introspectionState == IntrospectionState.FULLY_INTROSPECTED; - } - - void includeMixedInMembers(final Supplier introspectionContextProvider) { - introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, introspectionContextProvider); - mixedInMembersAdder.trigger(()->{ - var associationsInOrder = createMixedInAssociationsAndResort(computedMembers.associationsInOrder().toList()); - var actionsInOrder = createMixedInActionsAndResort(computedMembers.actionsInOrder().toList()); - - spec.replaceMembers(new ComputedMembers(Can.ofCollection(associationsInOrder), Can.ofCollection(actionsInOrder))); - }); - } - - /** - * @param introspectionContextProvider keeps track of the causal chain of introspection requests - */ - void introspectUpTo(final IntrospectionState upTo, final Supplier introspectionContextProvider) { - if(!isLessThan(upTo)) - return; // optimization - - if(log.isDebugEnabled()) { - log.debug("introspectingUpTo: {}, {}", spec.getFullIdentifier(), upTo); - } - - switch (introspectionState) { - case NOT_INTROSPECTED->{ - if(isLessThan(upTo)) { - introspectType(); - } - if(isLessThan(upTo)) { - introspectFully(); - spec.specLoaderInternal().validateLater(spec, introspectionContextProvider); - } - } - case TYPE_BEING_INTROSPECTED->{} // nothing to do (interim state during introspectType) - case TYPE_INTROSPECTED->{ - if(isLessThan(upTo)) { - introspectFully(); - spec.specLoaderInternal().validateLater(spec, introspectionContextProvider); - } - } - case MEMBERS_BEING_INTROSPECTED->{}// nothing to do (interim state during introspect fully) - case FULLY_INTROSPECTED->{}// nothing to do ... all done - } - } - - private boolean isLessThan(final IntrospectionState upTo) { - return introspectionState.isLessThan(upTo); - } - - private void introspectType() { - // set to avoid infinite loops - this.introspectionState = IntrospectionState.TYPE_BEING_INTROSPECTED; - introspectTypeHierarchy(); - spec.invalidateCachedFacets(); - this.introspectionState = IntrospectionState.TYPE_INTROSPECTED; - } - - private void introspectTypeHierarchy() { - spec.facetedMethodsBuilder.introspectClass(); - - // name - spec.addNamedFacetIfRequired(); - - // go no further if a value - if(spec.isValue()) { - if (log.isDebugEnabled()) { - log.debug("skipping type hierarchy introspection for value type {}", spec.getFullIdentifier()); - } - return; - } - - spec.loadSpecOfSuperclass(spec.getCorrespondingClass().getSuperclass()); - spec.loadSpecOfInterfaces(spec.getCorrespondingClass().getInterfaces()); - } - - private void introspectMembers() { - - // yet this logic does not skip UNKNONW - if(spec.getBeanSort().isCollection() - || spec.getBeanSort().isVetoed() - || spec.isValue()) { - if (log.isDebugEnabled()) { - log.debug("skipping full introspection for {} type {}", spec.getBeanSort(), spec.getFullIdentifier()); - } - return; - } - - // create associations and actions - this.computedMembers = new ComputedMembers( - Stream.concat( - createAssociations(spec.facetedMethodsBuilder), - createMixedInAssociations()), - Stream.concat( - createActions(spec.facetedMethodsBuilder), - createMixedInActions())); - - spec.replaceMembers(computedMembers); - - spec.postProcessor.postProcess(spec); - spec.invalidateCachedFacets(); - } - - 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(spec, null); - } - - // -- ASSOC CREATION - - private Stream createAssociations(final FacetedMethodsBuilder facetedMethodsBuilder) { - 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; - } - - // -- ACTION CREATION - - private Stream createActions(final FacetedMethodsBuilder facetedMethodsBuilder) { - 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. */ - 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; - } - - // -- MIXED IN MEMBERS - - /** - * Creates all mixed in properties and collections for this spec. - */ - private Stream createMixedInAssociations() { - if(true) return Stream.empty(); //FIXME - if (spec.isInjectable() || spec.isValue()) - return Stream.empty(); - return spec.getCausewayBeanTypeRegistry().streamMixinTypes() - .flatMap(this::createMixedInAssociation); - } - - private Stream createMixedInAssociation(final Class mixinType) { - var mixinSpec = spec.specLoaderInternal().loadSpecification(mixinType, - IntrospectionRequest.FULL); - if (mixinSpec == null - || mixinSpec == spec) - 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(spec.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(spec, mixinSpec, mixinMethodName)); - } - - // -- mixin actions - /** - * Creates all mixed in actions for this spec. - */ - private Stream createMixedInActions() { - if(true) return Stream.empty(); //FIXME - return spec.getCausewayBeanTypeRegistry().streamMixinTypes() - .flatMap(this::createMixedInAction); - } - - private Stream createMixedInAction(final Class mixinType) { - var mixinSpec = spec.specLoaderInternal().loadSpecification(mixinType, - IntrospectionRequest.FULL); - if (mixinSpec == null - || mixinSpec == spec) - 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(spec.getCorrespondingClass())) - return Stream.empty(); - // don't mixin Object_ mixins to domain services - if(spec.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(spec, mixinSpec, mixinMethodName)); - } - - /** - * 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.getBeanSort().isValue() - ? Objects.equals(spec, act.getReturnType()) - : true; - } - - // -- - - /** - * one-shot: must be no-op, if already created - * @return - */ - private List createMixedInActionsAndResort(final List regularActions) { - var include = spec.isEntityOrViewModelOrAbstract() - || spec.getBeanSort().isManagedBeanContributing() - // in support of composite value-type constructor mixins - || spec.getBeanSort().isValue(); - if(!include) - return regularActions; - var mixedInActions = createMixedInActions() - .collect(Collectors.toList()); - if(mixedInActions.isEmpty()) - return regularActions; // nothing to do (this spec has no mixed-in actions, regular actions have already been added) - - // note: we are doing this before any member sorting - _MemberIdClashReporting.flagAnyMemberIdClashes(spec, regularActions, mixedInActions); - - return _MemberSortingUtils.sortActionsIntoList(Stream.concat( - regularActions.stream(), - mixedInActions.stream())); - } - - /** - * one-shot: must be no-op, if already created - */ - private List createMixedInAssociationsAndResort(final List regularAssociations) { - if(!spec.isEntityOrViewModelOrAbstract()) - return regularAssociations; - var mixedInAssociations = createMixedInAssociations() - .collect(Collectors.toList()); - if(mixedInAssociations.isEmpty()) - return regularAssociations; // nothing to do (this spec has no mixed-in associations, regular associations have already been added) - - // note: we are doing this before any member sorting - _MemberIdClashReporting.flagAnyMemberIdClashes(spec, regularAssociations, mixedInAssociations); - - return _MemberSortingUtils.sortAssociationsIntoList(Stream.concat( - regularAssociations.stream(), - mixedInAssociations.stream())); - } - - -// 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))); -// } - -} \ No newline at end of file diff --git a/core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecification2.java b/core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecification2.java deleted file mode 100644 index 39ea2ba1df1..00000000000 --- a/core/metamodel/wip/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecification2.java +++ /dev/null @@ -1,821 +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 static org.apache.causeway.commons.internal.base._NullSafe.stream; - -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.stream.Stream; - -import org.apache.causeway.applib.Identifier; -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._Lazy; -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.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.ImperativeFacet; -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; -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.immutable.ImmutableFacet; -import org.apache.causeway.core.metamodel.facets.object.introspection.IntrospectionPolicyFacet; -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.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.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.spec.impl.MemberPopulator.ComputedMembers; -import org.apache.causeway.core.metamodel.spec.impl.MemberPopulator.IntrospectionState; -import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure; -import org.apache.causeway.core.metamodel.spi.EntityTitleSubscriber; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; -import org.springframework.util.ClassUtils; - -import lombok.Getter; -import lombok.experimental.Accessors; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -final class ObjectSpecification2 -implements ObjectMemberContainer, ObjectSpecificationMutable, HasSpecificationLoaderInternal { - - // -- CONSTRUCTION - - /** - * Lazily built by {@link #getMember(Method)}. - */ - private Map membersByMethod = null; - - final FacetedMethodsBuilder facetedMethodsBuilder; - private final ClassSubstitutorRegistry classSubstitutorRegistry; - private final _MembersAsColumns columnHelper; - private final _Lazy isInjectableLazy; - - @Getter(onMethod_={@Override}) - private final IntrospectionPolicy introspectionPolicy; - - @Getter @Accessors(fluent = true) - private final CausewayBeanMetaData typeMeta; - - public ObjectSpecification2( - final @NonNull CausewayBeanMetaData typeMeta, - final @NonNull MetaModelContext mmc, - final @NonNull FacetProcessor facetProcessor, - final @NonNull PostProcessor postProcessor, - final @NonNull ClassSubstitutorRegistry classSubstitutorRegistry) { - - this.typeMeta = typeMeta; - this.isInjectableLazy = _Lazy.threadSafe(()->typeMeta.isInjectable(getServiceRegistry())); - - this.facetHolder = FacetHolder.simple( - facetProcessor.getMetaModelContext(), - Identifier.classIdentifier(logicalType())); - - this.facetProcessor = facetProcessor; - this.postProcessor = postProcessor; - - 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) - .map(IntrospectionPolicyFacet::getIntrospectionPolicy) - .orElseGet(()->mmc.getConfiguration().core().metaModel().introspector().policy()); - - this.facetedMethodsBuilder = - new FacetedMethodsBuilder(this, facetProcessor, classSubstitutorRegistry); - - this.columnHelper = new _MembersAsColumns(mmc); - this.memberPopulator = new MemberPopulator2(this); - } - - @Override public BeanSort getBeanSort() { 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 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(), getBeanSort().name(), superclass() == null - ? "Object" - : superclass().getFullIdentifier()); - } - - void addNamedFacetIfRequired() { - if (getFacet(MemberNamedFacet.class) == null) { - addFacet(new MemberNamedFacetForStaticMemberName( - _Strings.asNaturalName.apply(getShortIdentifier()), - this)); - } - } - - // -- getObjectAction - - @Override - public Optional getDeclaredAction( - final @Nullable String id, - final ImmutableEnumSet actionScopes, - final MixedIn mixedIn) { - - memberPopulator.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) { - memberPopulator.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))); - } - - // -- 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); - } - - @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 - - final PostProcessor postProcessor; - final FacetProcessor facetProcessor; - - // -- 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 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 final MemberPopulator2 memberPopulator; - - @Getter(onMethod_ = {@Override}) private FacetHolder facetHolder; - - // -- Stuff immediately derivable from class - @Override - public final FeatureType getFeatureType() { - return FeatureType.OBJECT; - } - - @Override - public void introspect(final IntrospectionRequest request) { - memberPopulator.introspect(request); - } - - protected void loadSpecOfSuperclass(final Class superclass) { - if (superclass == null) - return; - - this.superclassSpec = specLoaderInternal().loadSpecification(superclass); - if (superclassSpec != null - && log.isDebugEnabled()) { - log.debug(" Superclass {}", superclass.getName()); - } - } - - protected void loadSpecOfInterfaces(final Class[] interfaces) { - if(interfaces==null) - return; - - var classCache = _ClassCache.getInstance(); - - final List interfaceSpecList = Stream.of(interfaces) - // pre-filter common interfaces (performance) - .filter(interfaceType->!interfaceType.getName().startsWith("java.")) - //-- - .map(interfaceType->{ - var substitution = classSubstitutorRegistry.getSubstitution(interfaceType); - return substitution.isReplace() - ? substitution.replacement() - : substitution.isNeverIntrospect() - ? null - : interfaceType; - }) - .filter(Objects::nonNull) - .filter(interfaceType->classCache.head(interfaceType).hasAnnotation(DomainObject.class)) - .map(specLoaderInternal()::loadSpecification) - .filter(Objects::nonNull) - .toList(); - - if(!interfaceSpecList.isEmpty()) { - if(interfaceSpecList.size()>1) { - ValidationFailure.raiseFormatted(facetHolder, - "Cannot use @DomainObject on more than one interface, as inherited by: %s", - getCorrespondingClass().getName()); - } - if (superclassSpec != null) { - var superType = superclassSpec.getCorrespondingClass(); - if(classCache.head(superType).hasAnnotation(DomainObject.class)) { - ValidationFailure.raiseFormatted(facetHolder, - "Cannot use @DomainObject on both, abstract super class and one interface, as inherited by: %s", - getCorrespondingClass().getName()); - } - } - -//debug -// System.err.println("%s".formatted(getCorrespondingClass().getName())); -// interfaceSpecList.forEach(i->{ -// System.err.println("- %s".formatted(i.getCorrespondingClass().getName())); -// }); - synchronized(unmodifiableInterfaces) { - this.interfaces.clear(); - this.interfaces.addAll(interfaceSpecList); - unmodifiableInterfaces.clear(); - } - } - } - - void invalidateCachedFacets() { - this.valueFacet = getFacet(ValueFacet.class); - this.titleFacet = lookupNonFallbackFacet(TitleFacet.class).orElse(null); - this.iconFacet = getFacet(IconFacet.class); - this.navigableParentFacet = getFacet(NavigableParentFacet.class); - this.cssClassFacet = getFacet(CssClassFacet.class); - this.aliasedFacet = getFacet(AliasedFacet.class); - } - - @Override - public final Optional> valueFacet() { - if(valueFacet == null - && getBeanSort().isValue()) { - invalidateCachedFacets(); - } - return Optional.ofNullable(valueFacet); - } - - @Override - public final Optional mixinFacet() { - // deliberately don't memoize lookup misses, because could be too early - if(mixinFacet==null) { - mixinFacet = getFacet(MixinFacet.class); - } - return Optional.ofNullable(mixinFacet); - } - - @Override - public final Optional entityFacet() { - // deliberately don't memoize lookup misses, because could be too early - if(entityFacet==null) { - entityFacet = getFacet(EntityFacet.class); - } - return Optional.ofNullable(entityFacet); - } - - @Override - public final Optional viewmodelFacet() { - // deliberately don't memoize lookup misses, because could be too early - if(viewmodelFacet==null) { - viewmodelFacet = getFacet(ViewModelFacet.class); - } - return Optional.ofNullable(viewmodelFacet); - } - - @Override - public String getTitle(final TitleRenderRequest titleRenderRequest) { - if (titleFacet != null) { - var titleString = titleFacet.title(titleRenderRequest); - if (!_Strings.isEmpty(titleString)) { - notifySubscribersIfEntity(titleRenderRequest, titleString); - return titleString; - } - } - var prefix = this.isInjectable() - ? "" - : "Untitled "; - 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 - ? navigableParentFacet.navigableParent(object) - : null; - } - - @Override - public String getCssClass(final ManagedObject reference) { - return cssClassFacet != null - ? cssClassFacet.cssClass(reference) - : null; - } - - @Override - public Can getAliases() { - return aliasedFacet != null - ? aliasedFacet.getAliases() - : Can.empty(); - } - - // -- ICON - - @Override - public Optional getIcon(final ManagedObject domainObject, final ObjectSupport.IconSize iconSize) { - if(ManagedObjects.isSpecified(domainObject)) { - _Assert.assertEquals(domainObject.objSpec(), this); - } - return Optional.ofNullable(iconFacet) - .flatMap(facet->facet.icon(domainObject, iconSize)) - .or(()->faLayers(domainObject) - .map(ObjectSupport.FontAwesomeIconResource::new)); - } - - private Optional faLayers(final ManagedObject domainObject){ - return lookupFacet(FaFacet.class) - .map(FaFacet::getSpecialization) - .map(either->either.fold( - faStaticFacet->(FaLayersProvider)faStaticFacet, - faImperativeFacet->faImperativeFacet.getFaLayersProvider(domainObject))) - .map(FaLayersProvider::getLayers); - } - - // -- HIERARCHICAL - - @Override - public boolean isOfType(final ObjectSpecification other) { - - var thisClass = this.getCorrespondingClass(); - var otherClass = other.getCorrespondingClass(); - - return thisClass == otherClass - || otherClass.isAssignableFrom(thisClass); - } - - @Override - public boolean isOfTypeResolvePrimitive(final ObjectSpecification other) { - - var thisClass = ClassUtils.resolvePrimitiveIfNecessary(this.getCorrespondingClass()); - var otherClass = ClassUtils.resolvePrimitiveIfNecessary(other.getCorrespondingClass()); - - return thisClass == otherClass - || otherClass.isAssignableFrom(thisClass); - } - - // -- NAME, DESCRIPTION, PERSISTABILITY - - @Override - public String getSingularName() { - return lookupFacet(ObjectNamedFacet.class) - .flatMap(ObjectNamedFacet::translated) - // unexpected code reach, however keep for JUnit testing - .orElseGet(()->String.format( - "(%s has neither title- nor object-named-facet)", - getFullIdentifier())); - } - - /** - * The translated description according to any available {@link ObjectDescribedFacet}, - * else empty string (""). - */ - @Override - public String getDescription() { - return lookupFacet(ObjectDescribedFacet.class) - .map(ObjectDescribedFacet::translated) - .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() - .map(MixinFacet::contributing); - } - - // -- FACET HANDLING - - @Override - public Q getFacet(final Class facetType) { - synchronized(unmodifiableInterfaces) { - return Hierarchical.lookupFacet(facetType, facetHolder, this) - .orElse(null); - } - } - - @Override - public ObjectTitleContext createTitleInteractionContext( - final ManagedObject targetObjectAdapter, - final InteractionInitiatedBy interactionMethod) { - - return new ObjectTitleContext(targetObjectAdapter, getFeatureIdentifier(), - targetObjectAdapter.getTitle(), - interactionMethod); - } - - // -- INHERITED - - @Override - public ObjectSpecification superclass() { - return superclassSpec; - } - - @Override - public Can interfaces() { - return unmodifiableInterfaces.get(); - } - - // -- ASSOCIATIONS - - @Override - public Stream streamDeclaredAssociations(final MixedIn mixedIn) { - memberPopulator.includeMixedInMembers( - ()->"streamDeclaredAssociations of %s".formatted(this.getFeatureIdentifier())); - - synchronized(unmodifiableAssociations) { - return stream(unmodifiableAssociations.get()) - .filter(mixedIn.toFilter()); - } - } - - @Override - public Optional getMember(final String memberId) { - memberPopulator.introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, - ()->"getMember %s of %s".formatted(memberId, this.getFeatureIdentifier())); - - 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 getDeclaredAssociation(final String id, final MixedIn mixedIn) { - memberPopulator.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) { - memberPopulator.includeMixedInMembers( - ()->"streamDeclaredActions of %s".formatted(this.getFeatureIdentifier())); - - return actionScopes.stream() - .flatMap(actionScope->stream(objectActionsByType.get(actionScope))) - .filter(mixedIn.toFilter()); - } - - // -- 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); - } - - @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 memberPopulator.isFullyIntrospected(); - } - - @Deprecated - void replaceMembers(final ComputedMembers computedMembers) { - synchronized (unmodifiableAssociations) { - associations.clear(); - associations.addAll(computedMembers.associationsInOrder().toList()); - unmodifiableAssociations.clear(); // invalidate - } - synchronized (unmodifiableActions) { - objectActions.clear(); - objectActions.addAll(computedMembers.actionsInOrder().toList()); - unmodifiableActions.clear(); // invalidate - // rebuild objectActionsByType multi-map - for (var actionType : ActionScope.values()) { - var objectActionForType = objectActionsByType.getOrElseNew(actionType); - objectActionForType.clear(); - computedMembers.actionsInOrder().stream() - .filter(ObjectAction.Predicates.ofActionType(actionType)) - .forEach(objectActionForType::add); - } - } - } - -// @Deprecated -// protected final void replaceAssociations(final Stream associations) { -// var orderedAssociations = _MemberSortingUtils.sortAssociationsIntoList(associations); -// synchronized (unmodifiableAssociations) { -// this.associations.clear(); -// this.associations.addAll(orderedAssociations); -// unmodifiableAssociations.clear(); // invalidate -// } -// } -// -// @Deprecated -// 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); -// } -// } -// } - -} From e284574699c1501bac45d3ce206825b44eca0052 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Wed, 29 Jul 2026 07:29:10 +0200 Subject: [PATCH 03/22] CAUSEWAY-4044: removes getHelp() - not used --- .../core/metamodel/spec/ObjectSpecification.java | 14 ++------------ .../spec/ObjectSpecificationRecord.java | 5 ----- .../spec/impl/ObjectSpecificationDefault.java | 16 ++-------------- 3 files changed, 4 insertions(+), 31 deletions(-) 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..41607e62c0b 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; @@ -78,6 +74,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; @@ -227,14 +225,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). 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 23181a5722f..75434568b08 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 @@ -242,11 +242,6 @@ public String getDescription() { return null; } @Override - public String getHelp() { - // TODO Auto-generated method stub - return null; - } - @Override public String getTitle(final TitleRenderRequest titleRenderRequest) { // TODO Auto-generated method stub return null; 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 14aad3b5cc7..98dd0374235 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 @@ -68,7 +68,6 @@ 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; @@ -759,16 +758,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() @@ -778,10 +767,9 @@ public final Optional contributing() { // -- FACET HANDLING @Override - public Q getFacet(final Class facetType) { + public Optional lookupFacet(final Class facetType) { synchronized(unmodifiableInterfaces) { - return Hierarchical.lookupFacet(facetType, facetHolder, this) - .orElse(null); + return Hierarchical.lookupFacet(facetType, facetHolder, this); } } From fab98cdf9389a591124d6721995738623406b851 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Wed, 29 Jul 2026 08:30:17 +0200 Subject: [PATCH 04/22] CAUSEWAY-4044: renames getBeanSort -> beanSort --- .../metamodel/commons/MetaModelVisitor.java | 4 +- ...ypeToBeIncludedWithMetamodelValidator.java | 2 +- .../action/ActionOverloadingValidator.java | 2 +- .../RemoveAnnotatedMethodsFacetFactory.java | 9 +-- .../javalang/RemoveMethodsFacetFactory.java | 13 ++-- .../LogicalTypeFacetFromClassNameFactory.java | 13 ++-- .../facets/object/mixin/MixinFacet.java | 1 + .../viewmodel/ViewModelFacetFactory.java | 6 +- ...ewModelFacetForDomainObjectAnnotation.java | 12 ++- .../metamodel/inspect/model/TypeNode.java | 4 +- .../interactions/InteractionHead.java | 50 ++++++------- .../object/ManagedObjectService.java | 7 +- .../core/metamodel/object/MmEntityUtils.java | 49 +++++------- .../core/metamodel/object/MmSpecUtils.java | 17 ++--- .../memento/ObjectDementifierFactory.java | 2 +- .../all/SanityChecksValidator.java | 6 +- .../ChoicesAndDefaultsPostProcessor.java | 2 +- .../ApplicationFeatureRepositoryDefault.java | 8 +- .../services/metamodel/MetaModelExporter.java | 32 ++++---- .../metamodel/MetaModelServiceDefault.java | 22 +++--- .../metamodel/spec/ObjectSpecification.java | 34 ++++----- .../spec/ObjectSpecificationRecord.java | 4 +- .../spec/impl/MixedInMemberFactory.java | 32 ++++---- .../spec/impl/ObjectSpecificationDefault.java | 75 ++++++------------- .../spec/impl/SpecificationLoaderDefault.java | 31 ++++---- .../impl/SpecificationLoaderInternal.java | 11 ++- ...notationEnforcesMetamodelContribution.java | 7 +- .../wrapper/WrapperFactoryDefault.java | 41 +++++----- .../excel/testing/ExcelFixture.java | 15 ++-- .../DomainModelTest_usingGoodDomain.java | 55 +++++++------- .../viewer/graphql/model/context/Context.java | 21 +++--- .../model/domain/common/query/CommonMeta.java | 2 +- .../query/CommonTopLevelQueryAbstract.java | 18 ++--- .../common/query/ObjectFeatureUtils.java | 4 +- .../rich/mutation/RichMutationForAction.java | 48 ++++++------ .../model/domain/rich/query/RichAction.java | 61 ++++++--------- .../rich/query/RichActionInvokeResult.java | 34 ++++----- .../domain/rich/scenario/ScenarioStep.java | 25 +++---- .../mutation/SimpleMutationForAction.java | 48 ++++++------ .../domain/simple/query/SimpleAction.java | 74 ++++++++---------- .../model/types/TypeMapperDefault.java | 55 +++++++------- .../wicket/model/models/ParameterModel.java | 7 +- .../widgets/actionlink/ActionLink.java | 2 +- 43 files changed, 417 insertions(+), 548 deletions(-) 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/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/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..7acd65cdd64 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, 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/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/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/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/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/metamodel/MetaModelExporter.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelExporter.java index 1b9b4cab7e4..9cb80f14124 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 @@ -80,9 +80,8 @@ MetamodelDto exportMetaModel(final Config config) { .peek(specIfAny->specIfAny.ifPresent(tinyDomain::add)) .allMatch(Optional::isPresent); - if(useTinyDomain) { - return exportTinyDomain(tinyDomain, config); - } + if(useTinyDomain) + return exportTinyDomain(tinyDomain, config); MetamodelDto metamodelDto = new MetamodelDto(); @@ -130,9 +129,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 +192,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 +226,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 +429,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 +454,7 @@ private void sortFacets(final Listfilter.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/ObjectSpecification.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/ObjectSpecification.java index 41607e62c0b..7004a0e1c0c 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 @@ -102,7 +102,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 = @@ -268,11 +268,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(); @@ -335,7 +335,7 @@ default boolean isSingular() { * @see #isSingular() */ default boolean isPlural() { - return getBeanSort().isCollection(); + return beanSort().isCollection(); } /** @@ -345,7 +345,7 @@ default boolean isPlural() { * In effect, means has got {@link ValueFacet}. */ default boolean isValue() { - return getBeanSort().isValue() + return beanSort().isValue() || valueFacet().isPresent(); } @@ -353,7 +353,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); } @@ -401,7 +401,7 @@ default boolean isValueOrIsParented() { boolean isDomainService(); default boolean isMixin() { - return getBeanSort().isMixin(); + return beanSort().isMixin(); } /** @@ -429,7 +429,7 @@ default boolean isPrimitive() { } default boolean isAbstract() { - return getBeanSort().isAbstract(); + return beanSort().isAbstract(); } /** @@ -448,8 +448,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()); } @@ -457,8 +457,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()); } @@ -491,13 +491,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 75434568b08..32db636d3e2 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 @@ -201,14 +201,14 @@ public boolean equals(final Object o) { @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()); } // -- COMPONENTS AND GETTERS - @Override public BeanSort getBeanSort() { return typeMeta.beanSort(); } + @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(); } 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 index 3e90906811f..aa7ec802096 100644 --- 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 @@ -35,43 +35,43 @@ record MixedInMemberFactory( ObjectSpecification spec, SpecificationLoaderInternal specLoaderInternal, CausewayBeanTypeRegistry causewayBeanTypeRegistry) { - + MixedInMemberFactory( - ObjectSpecification spec, - SpecificationLoaderInternal specLoaderInternal) { + final ObjectSpecification spec, + final SpecificationLoaderInternal specLoaderInternal) { this(spec, specLoaderInternal, spec.getServiceRegistry() .lookupServiceElseFail(CausewayBeanTypeRegistry.class)); } - + /** * Creates all mixed in properties and collections for this spec. */ public List createMixedInAssociations() { - var include = spec.isEntityOrViewModelOrAbstract() - && !spec.isInjectable() + var include = spec.isEntityOrViewModelOrAbstract() + && !spec.isInjectable() && !spec.isValue(); - return include + return include ? causewayBeanTypeRegistry.streamMixinTypes() .flatMap(this::createMixedInAssociation) .toList() : List.of(); } - + /** * Creates all mixed in actions for this spec. */ public List createMixedInActions() { var include = spec.isEntityOrViewModelOrAbstract() - || spec.getBeanSort().isManagedBeanContributing() + || spec.beanSort().isManagedBeanContributing() // in support of composite value-type constructor mixins - || spec.getBeanSort().isValue(); + || spec.beanSort().isValue(); return include ? causewayBeanTypeRegistry.streamMixinTypes() .flatMap(this::createMixedInAction) .toList() : List.of(); } - + // -- HELPER private Stream createMixedInAssociation(final Class mixinType) { @@ -93,7 +93,7 @@ private Stream createMixedInAssociation(final Class mixinT .map(ObjectActionDefault.class::cast) .map(mixedInAssociation(spec, mixinSpec, mixinMethodName)); } - + private Stream createMixedInAction(final Class mixinType) { var mixinSpec = specLoaderInternal.loadSpecification(mixinType, @@ -108,7 +108,7 @@ private Stream createMixedInAction(final Class mixinType if(!mixinFacet.isMixinFor(spec.getCorrespondingClass())) return Stream.empty(); // don't mixin Object_ mixins to domain services - if(spec.getBeanSort().isManagedBeanContributing() + if(spec.beanSort().isManagedBeanContributing() && mixinFacet.isMixinFor(java.lang.Object.class)) return Stream.empty(); @@ -129,11 +129,11 @@ private Stream createMixedInAction(final Class mixinType * also to support associated Actions for Action Parameters. */ private boolean whenIsValueThenIsAlsoConstructorMixin(final ObjectAction act) { - return spec.getBeanSort().isValue() + return spec.beanSort().isValue() ? Objects.equals(spec, act.getReturnType()) : true; } - + private static Function mixedInAction( final ObjectSpecification mixeeSpec, final ObjectSpecification mixinSpec, @@ -155,5 +155,5 @@ private static Function mixedInAssociati : new OneToManyAssociationMixedIn( mixeeSpec, mixinAction, mixinSpec, mixinMethodName); } - + } 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 98dd0374235..e1693e68416 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 @@ -129,10 +129,11 @@ final class ObjectSpecificationDefault private final ClassSubstitutorRegistry classSubstitutorRegistry; private final _MembersAsColumns columnHelper; private final _Lazy isInjectableLazy; + private final _Lazy isDomainServiceLazy; @Getter(onMethod_={@Override}) private final IntrospectionPolicy introspectionPolicy; - + @Getter @Accessors(fluent = true) private final CausewayBeanMetaData typeMeta; @@ -145,6 +146,8 @@ public ObjectSpecificationDefault( this.typeMeta = typeMeta; this.isInjectableLazy = _Lazy.threadSafe(()->typeMeta.isInjectable(getServiceRegistry())); + this.isDomainServiceLazy = _Lazy.threadSafe(()-> + _ClassCache.getInstance().head(getCorrespondingClass()).hasAnnotation(DomainService.class)); this.facetHolder = FacetHolder.simple( facetProcessor.getMetaModelContext(), @@ -157,7 +160,7 @@ public ObjectSpecificationDefault( 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()); @@ -166,19 +169,18 @@ public ObjectSpecificationDefault( this.columnHelper = new _MembersAsColumns(mmc); } - - @Override public BeanSort getBeanSort() { return typeMeta.beanSort(); } + + @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); } - + @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 @@ -195,7 +197,7 @@ public boolean equals(final Object o) { @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()); } @@ -222,17 +224,17 @@ protected void introspectTypeHierarchy() { private void introspectMembers() { // 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; } var memberFactory = new RegularMemberFactory(this, facetedMethodsBuilder); - + // create associations and actions replaceAssociations(memberFactory.createAssociations()); replaceActions(memberFactory.createActions()); @@ -366,18 +368,7 @@ public Stream streamActionsForColumnRendering(final Where where) { return columnHelper.streamActionsForColumnRendering(this, where); } - @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 @@ -432,7 +423,7 @@ public boolean isDomainService() { private IntrospectionState introspectionState = IntrospectionState.NOT_INTROSPECTED; - @Getter(onMethod_ = {@Override}) private FacetHolder facetHolder; + @Getter(onMethod_ = {@Override}) private final FacetHolder facetHolder; // -- Stuff immediately derivable from class @Override @@ -610,7 +601,7 @@ void invalidateCachedFacets() { @Override public final Optional> valueFacet() { if(valueFacet == null - && getBeanSort().isValue()) { + && beanSort().isValue()) { invalidateCachedFacets(); } return Optional.ofNullable(valueFacet); @@ -717,7 +708,6 @@ private Optional faLayers(final ManagedObject domainObject){ @Override public boolean isOfType(final ObjectSpecification other) { - var thisClass = this.getCorrespondingClass(); var otherClass = other.getCorrespondingClass(); @@ -727,7 +717,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()); @@ -765,7 +754,7 @@ public final Optional contributing() { } // -- FACET HANDLING - + @Override public Optional lookupFacet(final Class facetType) { synchronized(unmodifiableInterfaces) { @@ -773,7 +762,7 @@ public Optional lookupFacet(final Class facetType) { } } - @Override + @Override //FIXME separation of concerns public ObjectTitleContext createTitleInteractionContext( final ManagedObject targetObjectAdapter, final InteractionInitiatedBy interactionMethod) { @@ -893,22 +882,6 @@ public ObjectValidityContext createValidityInteractionContext( 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 mixedInMemberAdder = new _Oneshot(); @@ -921,8 +894,8 @@ private void createMixedInMembersAndResort() { createMixedInActionsAndResort(memberFactory); createMixedInAssociationsAndResort(memberFactory); } - - private void createMixedInActionsAndResort(MixedInMemberFactory memberFactory) { + + private void createMixedInActionsAndResort(final MixedInMemberFactory memberFactory) { var mixedInActions = memberFactory.createMixedInActions(); if(mixedInActions.isEmpty()) return; // nothing to do (this spec has no mixed-in actions, regular actions have already been added) @@ -937,7 +910,7 @@ private void createMixedInActionsAndResort(MixedInMemberFactory memberFactory) { mixedInActions.stream())); } - private void createMixedInAssociationsAndResort(MixedInMemberFactory memberFactory) { + private void createMixedInAssociationsAndResort(final MixedInMemberFactory memberFactory) { var mixedInAssociations = memberFactory.createMixedInAssociations(); if(mixedInAssociations.isEmpty()) return; // nothing to do (this spec has no mixed-in associations, regular associations have already been added) 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..33f2ec85300 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 @@ -34,19 +34,6 @@ 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; @@ -86,7 +73,17 @@ 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; @@ -221,7 +218,7 @@ record SpecCollector( public void collect(final @Nullable ObjectSpecificationMutable 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); @@ -283,8 +280,8 @@ public void createMetaModel() { 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); + introspect(snapshot.filter(x->x.beanSort().isMixin()), IntrospectionRequest.FULL); + introspect(snapshot.filter(x->!x.beanSort().isMixin()), IntrospectionRequest.FULL); } log.info(" - running remaining validators"); @@ -492,7 +489,7 @@ public void addValidationFailure(final ValidationFailure validationFailure) { } } - private _Lazy validationResult = + private final _Lazy validationResult = _Lazy.threadSafe(this::runMetaModelValidators); private final AtomicBoolean validationInProgress = new AtomicBoolean(false); 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 5f35b96adba..77be0f9cc31 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 @@ -21,11 +21,6 @@ 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; import org.apache.causeway.applib.services.metamodel.BeanSort; @@ -35,6 +30,10 @@ import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.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 { @@ -152,7 +151,7 @@ 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(); } 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/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/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/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 a83a73eefab..eb57afd804e 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,22 +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.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 static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -41,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; @@ -125,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( @@ -393,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"); @@ -403,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"); @@ -411,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"); } @@ -432,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 @@ -446,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()); } @@ -461,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"); @@ -471,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"); @@ -479,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"); @@ -499,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)); } @@ -508,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"); @@ -1057,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/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(), From deadecba00030163fa05981125b3c6f52185c8e5 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Wed, 29 Jul 2026 10:27:20 +0200 Subject: [PATCH 05/22] CAUSEWAY-4044: work obj spec record --- .../DomainObjectAnnotationFacetFactory.java | 4 +- .../metamodel/MetaModelServiceDefault.java | 2 +- .../metamodel/spec/ObjectSpecification.java | 2 +- .../spec/ObjectSpecificationRecord.java | 233 ++++++++++-------- .../spec/impl/LogicalTypeResolver.java | 2 +- .../spec/impl/ObjectSpecificationDefault.java | 49 +++- .../spec/impl/SpecificationLoaderDefault.java | 8 +- .../DomainModelTest_usingGoodDomain.java | 4 +- 8 files changed, 184 insertions(+), 120 deletions(-) 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/services/metamodel/MetaModelServiceDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java index 9ecca914cd0..bff57be6c11 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java @@ -99,7 +99,7 @@ public Can 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); } 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 7004a0e1c0c..d9c809fe3b7 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 @@ -206,7 +206,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. 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 32db636d3e2..7f6f5869008 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,22 +18,24 @@ */ 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.DomainService; 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.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.base._Lazy; -import org.apache.causeway.commons.internal.reflection._ClassCache; +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.consent.Consent; @@ -42,27 +44,36 @@ 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.all.hide.HiddenFacet; +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.immutable.ImmutableFacet; -import org.apache.causeway.core.metamodel.facets.object.logicaltype.AliasedFacet; +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.parented.ParentedCollectionFacet; +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.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.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( CausewayBeanMetaData typeMeta, FeatureType featureType, @@ -70,63 +81,30 @@ public record ObjectSpecificationRecord( Hierarchical hierarchical, ObjectActionContainer actionContainer, ObjectAssociationContainer associationContainer, + Can titleSubscribers, IntrospectionPolicy introspectionPolicy, + Can aliases, Optional> valueFacet, Optional entityFacet, Optional viewmodelFacet, Optional mixinFacet, - _Lazy> aliases, - _Lazy isDomainServiceLazy, - _Lazy isInjectableLazy) + 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 ObjectSpecification { -// ObjectSpecificationRecord( -// final CausewayBeanMetaData typeMeta, -// final FeatureType featureType, -// final FacetHolder facetHolder, -// final Hierarchical hierarchical, -// final ObjectActionContainer actionContainer, -// final ObjectAssociationContainer associationContainer, -// final IntrospectionPolicy introspectionPolicy) { -// this(typeMeta, featureType, facetHolder, hierarchical, actionContainer, associationContainer, introspectionPolicy, -// null, null, null, null, -// _Lazy.threadSafe(()->Hierarchical.lookupFacet(AliasedFacet.class, facetHolder, hierarchical) -// .map(AliasedFacet::getAliases) -// .orElseGet(Can::empty)), -// _Lazy.threadSafe(()->_ClassCache.getInstance() -// .head(typeMeta.getCorrespondingClass()) -// .hasAnnotation(DomainService.class)) -// ); -// } - - public ObjectSpecificationRecord { - aliases = _Lazy.threadSafe(()->Hierarchical.lookupFacet(AliasedFacet.class, facetHolder, hierarchical) - .map(AliasedFacet::getAliases) - .orElseGet(Can::empty)); - isDomainServiceLazy = _Lazy.threadSafe(()->_ClassCache.getInstance() - .head(typeMeta.getCorrespondingClass()) - .hasAnnotation(DomainService.class)); - - boolean isVetoedForInjection = switch (typeMeta.managedBy()) { - case NONE, CAUSEWAY, PERSISTENCE -> true; - case UNSPECIFIED, SPRING -> false; - }; - - isInjectableLazy = _Lazy.threadSafe(()-> - !isVetoedForInjection - && !typeMeta.beanSort().isAbstract() - && !typeMeta.beanSort().isValue() - && !typeMeta.beanSort().isEntity() - && !typeMeta.beanSort().isViewModel() - && !typeMeta.beanSort().isMixin() - && (typeMeta.beanSort().isManagedBeanAny() - || getServiceRegistry() - .lookupRegisteredBeanById(typeMeta.logicalType()) - .isPresent())); - } - - // -- SPECIFICATION @Override public FeatureType getFeatureType() { return featureType; } @@ -214,85 +192,118 @@ public String toString() { @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); } @Override public Optional getMember(final String memberId) { - // TODO Auto-generated method stub - return Optional.empty(); + 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) { - // TODO Auto-generated method stub - return Optional.empty(); + return Optional.ofNullable(membersByMethod.get(method)); } @Override public String getSingularName() { - // TODO Auto-generated method stub - return null; + 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() { - // TODO Auto-generated method stub - return null; + return objectDescribedFacet + .map(ObjectDescribedFacet::translated) + .orElse(""); } @Override public String getTitle(final TitleRenderRequest titleRenderRequest) { - // TODO Auto-generated method stub - return null; + 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 object, final IconSize iconSize) { - // TODO Auto-generated method stub - return Optional.empty(); + 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) { - // TODO Auto-generated method stub - return null; + return navigableParentFacet + .map(facet->facet.navigableParent(object)) + .orElse(null); } @Override public String getCssClass(final ManagedObject domainObject) { - // TODO Auto-generated method stub - return null; + return cssClassFacet + .map(facet->facet.cssClass(domainObject)) + .orElse(null); } @Override public Optional explicitElementSpec() { - // TODO Auto-generated method stub - return Optional.empty(); + return typeOfFacet + .map(TypeOfFacet::elementSpec); } @Override public Optional contributing() { - // TODO Auto-generated method stub - return Optional.empty(); + return mixinFacet() + .map(MixinFacet::contributing); } - @Override + + @Override //TODO perhaps move - not the responsibility of a data carrier public ObjectTitleContext createTitleInteractionContext(final ManagedObject targetObjectAdapter, - final InteractionInitiatedBy invocationMethod) { - // TODO Auto-generated method stub - return null; + final InteractionInitiatedBy initiatedBy) { + return new ObjectTitleContext(targetObjectAdapter, getFeatureIdentifier(), + targetObjectAdapter.getTitle(), + initiatedBy); } + + // -- VALIDITY //TODO perhaps move - not the responsibility of a data carrier + @Override public ObjectValidityContext createValidityInteractionContext(final ManagedObject targetAdapter, final InteractionInitiatedBy interactionInitiatedBy) { - // TODO Auto-generated method stub - return null; - } - @Override - public Consent isValid(final ManagedObject targetAdapter, final InteractionInitiatedBy interactionInitiatedBy) { - // TODO Auto-generated method stub - return null; - } - @Override - public InteractionResult isValidResult(final ManagedObject targetAdapter, final InteractionInitiatedBy interactionInitiatedBy) { - // TODO Auto-generated method stub - return null; + return new ObjectValidityContext(targetAdapter, getFeatureIdentifier(), interactionInitiatedBy); } + @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); + } // -- FACET LOOKUP @@ -301,4 +312,26 @@ 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/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/ObjectSpecificationDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java index e1693e68416..b706940fcd8 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 @@ -98,6 +98,7 @@ 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.ObjectSpecificationRecord; 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; @@ -139,18 +140,19 @@ final class ObjectSpecificationDefault public ObjectSpecificationDefault( final @NonNull CausewayBeanMetaData typeMeta, - final @NonNull MetaModelContext mmc, final @NonNull FacetProcessor facetProcessor, final @NonNull PostProcessor postProcessor, final @NonNull ClassSubstitutorRegistry classSubstitutorRegistry) { + final MetaModelContext mmc = facetProcessor.getMetaModelContext(); + this.typeMeta = typeMeta; - this.isInjectableLazy = _Lazy.threadSafe(()->typeMeta.isInjectable(getServiceRegistry())); + 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(), + mmc, Identifier.classIdentifier(logicalType())); this.postProcessor = postProcessor; @@ -170,6 +172,41 @@ public ObjectSpecificationDefault( this.columnHelper = new _MembersAsColumns(mmc); } + // -- SHALLOW IMMUTABLE + + ObjectSpecificationRecord toRecord() { + return new ObjectSpecificationRecord( + typeMeta, + getFeatureType(), + facetHolder, + this,//Hierarchical, + this,//ObjectActionContainer + this,//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(), + catalogueMembers()); + } + + // -- + @Override public BeanSort beanSort() { return typeMeta.beanSort(); } @Override public Class getCorrespondingClass() { return typeMeta.getCorrespondingClass(); } @Override public LogicalType logicalType() { return typeMeta.logicalType(); } @@ -202,7 +239,7 @@ public String toString() { : superclass().getFullIdentifier()); } - protected void introspectTypeHierarchy() { + private void introspectTypeHierarchy() { facetedMethodsBuilder.introspectClass(); @@ -676,7 +713,7 @@ public String getCssClass(final ManagedObject reference) { } @Override - public Can getAliases() { + public Can aliases() { return aliasedFacet != null ? aliasedFacet.getAliases() : Can.empty(); @@ -762,7 +799,7 @@ public Optional lookupFacet(final Class facetType) { } } - @Override //FIXME separation of concerns + @Override //TODO separation of concerns ? public ObjectTitleContext createTitleInteractionContext( final ManagedObject targetObjectAdapter, final InteractionInitiatedBy interactionMethod) { 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 33f2ec85300..f502b26c0c4 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 @@ -59,7 +59,6 @@ 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.progmodel.ProgrammingModel; @@ -125,8 +124,6 @@ class SpecificationLoaderDefault @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<>(); @@ -179,7 +176,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) @@ -195,7 +191,6 @@ public void init() { if (log.isDebugEnabled()) { log.debug("initialising {}", this); } - this.metaModelContext = serviceRegistry.lookupServiceElseFail(MetaModelContext.class); this.facetProcessor = new FacetProcessor(programmingModel); } @@ -576,7 +571,7 @@ private ObjectSpecificationMutable loadSpecificationNullable( 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,7 +596,6 @@ private ObjectSpecificationMutable loadSpecificationNullable( private ObjectSpecificationMutable createSpecification(final CausewayBeanMetaData typeMeta) { var objectSpec = new ObjectSpecificationDefault( typeMeta, - metaModelContext, facetProcessor, postProcessor, classSubstitutorRegistry); 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 eb57afd804e..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 @@ -683,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") @@ -703,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") From 89cf225a258571f2db058883e1b463bb2d2478a6 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Wed, 29 Jul 2026 10:59:21 +0200 Subject: [PATCH 06/22] CAUSEWAY-4044: work on default obj spec to become a builder --- .../spec/impl/FacetedMethodsBuilder.java | 2 +- .../spec/impl/MixedInMemberFactory.java | 2 +- .../spec/impl/ObjectSpecificationBuilder.java | 57 +++++++++++++++++++ .../spec/impl/ObjectSpecificationDefault.java | 5 +- .../spec/impl/ObjectSpecificationMutable.java | 1 + .../spec/impl/SpecificationLoaderDefault.java | 36 ++++++------ .../impl/SpecificationLoaderInternal.java | 2 +- 7 files changed, 82 insertions(+), 23 deletions(-) create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationBuilder.java 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/FacetedMethodsBuilder.java index 80b37faf4a4..e99776d52f5 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/FacetedMethodsBuilder.java @@ -55,7 +55,7 @@ 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.ObjectSpecification; -import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionRequest; +import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationBuilder.IntrospectionRequest; import org.apache.causeway.core.metamodel.specloader.typeextract.TypeExtractor; import lombok.Getter; 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 index aa7ec802096..ac1aecf86e0 100644 --- 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 @@ -29,7 +29,7 @@ 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.impl.ObjectSpecificationMutable.IntrospectionRequest; +import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationBuilder.IntrospectionRequest; record MixedInMemberFactory( ObjectSpecification spec, 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..bcb8933d7c5 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationBuilder.java @@ -0,0 +1,57 @@ +/* + * 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.core.metamodel.spec.ObjectSpecification; + +interface ObjectSpecificationBuilder +extends + ObjectMemberContainer, + HasSpecificationLoaderInternal, + ObjectSpecification // TODO remove +// Specification, +// HasLogicalType, +// HasFacetHolder, +// Hierarchical, +// ObjectActionContainer, +// ObjectAssociationContainer, +// ObjectMemberContainer, +// HasSpecificationLoaderInternal + { + + 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 introspect(IntrospectionRequest request); + + ObjectSpecification build(); + +} 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 b706940fcd8..39a37464e4f 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 @@ -117,7 +117,7 @@ @Slf4j final class ObjectSpecificationDefault -implements ObjectMemberContainer, ObjectSpecificationMutable, HasSpecificationLoaderInternal { +implements ObjectSpecificationBuilder { // -- CONSTRUCTION @@ -174,7 +174,8 @@ public ObjectSpecificationDefault( // -- SHALLOW IMMUTABLE - ObjectSpecificationRecord toRecord() { + @Override + public ObjectSpecificationRecord build() { return new ObjectSpecificationRecord( typeMeta, getFeatureType(), 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/ObjectSpecificationMutable.java index 9ff20373e97..b4e1561d27a 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/ObjectSpecificationMutable.java @@ -20,6 +20,7 @@ import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +//renamed public interface ObjectSpecificationMutable extends ObjectSpecification { enum IntrospectionRequest { 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 f502b26c0c4..25c70824c72 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 @@ -67,7 +67,7 @@ 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.ObjectSpecificationBuilder.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; @@ -126,7 +126,7 @@ class SpecificationLoaderDefault private FacetProcessor facetProcessor; - private final Map, ObjectSpecificationMutable> cache = new ConcurrentHashMap<>(); + private final Map, ObjectSpecificationBuilder> cache = new ConcurrentHashMap<>(); private final LogicalTypeResolver logicalTypeResolver = new LogicalTypeResolver(); /** @@ -195,13 +195,13 @@ public void init() { } 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<>(), @@ -210,7 +210,7 @@ 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.beanSort()) { @@ -428,7 +428,7 @@ public void validateLater( // -- LOOKUP @Override - public Can snapshotSpecifications() { + public Can snapshotSpecifications() { return Can.ofCollection(cache.values()); } @@ -544,7 +544,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); @@ -552,7 +552,7 @@ private ObjectSpecificationMutable primeSpecification( } @Nullable - private ObjectSpecificationMutable loadSpecificationNullable( + private ObjectSpecificationBuilder loadSpecificationNullable( final @Nullable Class type, final @NonNull Function, CausewayBeanMetaData> beanClassifier, final @NonNull IntrospectionRequest request) { @@ -593,7 +593,7 @@ 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, facetProcessor, @@ -603,7 +603,7 @@ private ObjectSpecificationMutable createSpecification(final CausewayBeanMetaDat } private void introspectSequential( - final Can specs, + final Can specs, final IntrospectionRequest request) { for (var spec : specs) { spec.introspect(request); @@ -611,7 +611,7 @@ private void introspectSequential( } private void introspectParallel( - final Can specs, + final Can specs, final IntrospectionRequest request) { specs.parallelStream() .forEach(spec -> { @@ -626,7 +626,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); @@ -635,7 +635,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 77be0f9cc31..339baa0a11b 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 @@ -28,7 +28,7 @@ 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.ObjectSpecificationBuilder.IntrospectionRequest; import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; From 1ee3ec0f33b983d8cc75751ac1e06e18d2a1272b Mon Sep 17 00:00:00 2001 From: andi-huber Date: Wed, 29 Jul 2026 11:23:41 +0200 Subject: [PATCH 07/22] CAUSEWAY-4044: intermediate cleanup --- .../spec/impl/FacetedMethodsBuilder.java | 30 +++++++------------ .../spec/impl/ObjectSpecificationBuilder.java | 9 ++++-- .../spec/impl/ObjectSpecificationDefault.java | 3 +- 3 files changed, 20 insertions(+), 22 deletions(-) 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/FacetedMethodsBuilder.java index e99776d52f5..18d74880127 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/FacetedMethodsBuilder.java @@ -30,8 +30,6 @@ 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; @@ -54,9 +52,9 @@ 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.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationBuilder.IntrospectionRequest; import org.apache.causeway.core.metamodel.specloader.typeextract.TypeExtractor; +import org.jspecify.annotations.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -105,7 +103,7 @@ public Can snapshotMethodsRemaining() { } - private final ObjectSpecification inspectedTypeSpec; + private final ObjectSpecificationBuilder inspectedTypeSpec; @Getter private final Class introspectedClass; @@ -121,7 +119,7 @@ public Can snapshotMethodsRemaining() { // -- CONSTRUCTOR public FacetedMethodsBuilder( - final ObjectSpecification inspectedTypeSpec, + final ObjectSpecificationBuilder inspectedTypeSpec, final FacetProcessor facetProcessor, final ClassSubstitutorRegistry classSubstitutorRegistry) { @@ -400,14 +398,11 @@ private boolean representsAction(final ResolvedMethod actionMethod) { return true; } - //FIXME potentially misses other ObjectSpecification impl. - if(inspectedTypeSpec instanceof ObjectSpecificationDefault objspecDefault) { - // exclude those that have eg. reserved prefixes - if (getFacetProcessor().recognizes(actionMethod)) { - // this is a potential orphan candidate, collect these, than use when validating - objspecDefault.getPotentialOrphans().add(actionMethod); - return false; - } + // exclude those that have eg. reserved prefixes + if (getFacetProcessor().recognizes(actionMethod)) { + // this is a potential orphan candidate, collect these, than use when validating + inspectedTypeSpec.getPotentialOrphans().add(actionMethod); + return false; } if(introspectionPolicy().getMemberAnnotationPolicy().isMemberAnnotationsRequired()) { @@ -438,12 +433,9 @@ private boolean isMixinMain(final ResolvedMethod method) { .orElse(null); if(mixinFacet==null) return false; - //FIXME potentially misses other ObjectSpecification impl. - if(inspectedTypeSpec instanceof ObjectSpecificationDefault objspecDefault) { - if(!objspecDefault.isFullyIntrospected()) - // members are not introspected yet, so make a guess - return mixinFacet.isCandidateForMain(method); - } + if(!inspectedTypeSpec.isFullyIntrospected()) + // members are not introspected yet, so make a guess + return mixinFacet.isCandidateForMain(method); return inspectedTypeSpec .lookupMixedInAction(inspectedTypeSpec) 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 index bcb8933d7c5..6a613192385 100644 --- 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 @@ -18,6 +18,9 @@ */ 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; interface ObjectSpecificationBuilder @@ -50,8 +53,10 @@ enum IntrospectionRequest { FULL } - void introspect(IntrospectionRequest request); - ObjectSpecification build(); + void introspect(IntrospectionRequest request); + Set getPotentialOrphans(); + boolean isFullyIntrospected(); + } 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 39a37464e4f..09e165d5dbd 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 @@ -967,7 +967,8 @@ private void createMixedInAssociationsAndResort(final MixedInMemberFactory membe private final Can titleSubscribers = getServiceRegistry().select(EntityTitleSubscriber.class); - boolean isFullyIntrospected() { + @Override + public boolean isFullyIntrospected() { return this.introspectionState == IntrospectionState.FULLY_INTROSPECTED; } From 9c6ba82abeddd1209f570249a051984f6e215fc9 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Thu, 30 Jul 2026 09:00:31 +0200 Subject: [PATCH 08/22] CAUSEWAY-4044: refactors FacetedMethodsBuilder into a record --- .../CausewayModuleCoreMetamodel.java | 18 +- .../classsubstitutor/ClassSubstitutor.java | 2 +- .../ClassSubstitutorAbstract.java | 62 ++--- .../ClassSubstitutorDefault.java | 9 +- .../ClassSubstitutorForCollections.java | 17 +- .../ClassSubstitutorForDomainObjects.java | 21 +- ...uilder.java => FacetedMethodsFactory.java} | 236 +++++++----------- .../spec/impl/ObjectSpecificationDefault.java | 10 +- .../spec/impl/RegularMemberFactory.java | 16 +- 9 files changed, 147 insertions(+), 244 deletions(-) rename core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/{FacetedMethodsBuilder.java => FacetedMethodsFactory.java} (66%) 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/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/spec/impl/FacetedMethodsBuilder.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsFactory.java similarity index 66% 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 18d74880127..256ad92f99d 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,12 +19,12 @@ 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; @@ -35,15 +35,12 @@ 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; @@ -56,27 +53,31 @@ 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; + 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))); + } - private ConcurrentMethodRemover(final Class introspectedClass, final Stream methodStream) { - this.methodsRemaining = methodStream - .collect(Collectors.toCollection(_Sets::newConcurrentHashSet)); - } - - @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) { @@ -85,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 ObjectSpecificationBuilder 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 ObjectSpecificationBuilder 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) { @@ -222,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, @@ -263,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, @@ -282,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()) { @@ -338,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, @@ -356,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; } @@ -399,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; } @@ -429,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()) + 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) @@ -448,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/ObjectSpecificationDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java index 09e165d5dbd..7ce1d736dd1 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 @@ -126,7 +126,7 @@ final class ObjectSpecificationDefault */ private Map membersByMethod = null; - private final FacetedMethodsBuilder facetedMethodsBuilder; + private final FacetedMethodsFactory facetedMethodsFactory; private final ClassSubstitutorRegistry classSubstitutorRegistry; private final _MembersAsColumns columnHelper; private final _Lazy isInjectableLazy; @@ -166,8 +166,8 @@ public ObjectSpecificationDefault( .map(IntrospectionPolicyFacet::getIntrospectionPolicy) .orElseGet(()->mmc.getConfiguration().core().metaModel().introspector().policy()); - this.facetedMethodsBuilder = - new FacetedMethodsBuilder(this, facetProcessor, classSubstitutorRegistry); + this.facetedMethodsFactory = + new FacetedMethodsFactory(this, facetProcessor, classSubstitutorRegistry); this.columnHelper = new _MembersAsColumns(mmc); } @@ -242,7 +242,7 @@ public String toString() { private void introspectTypeHierarchy() { - facetedMethodsBuilder.introspectClass(); + facetedMethodsFactory.introspectClass(); // name addNamedFacetIfRequired(); @@ -271,7 +271,7 @@ private void introspectMembers() { return; } - var memberFactory = new RegularMemberFactory(this, facetedMethodsBuilder); + var memberFactory = new RegularMemberFactory(this, facetedMethodsFactory); // create associations and actions replaceAssociations(memberFactory.createAssociations()); 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 index 8c0e90affe2..e7276f039c7 100644 --- 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 @@ -30,22 +30,20 @@ record RegularMemberFactory( ObjectSpecification spec, - FacetedMethodsBuilder facetedMethodsBuilder) { - + FacetedMethodsFactory factory) { + Stream createAssociations() { - return facetedMethodsBuilder.getAssociationFacetedMethods() - .stream() + return factory.createAssociationFacetedMethods() .map(this::createAssociation) .filter(_NullSafe::isPresent); } - + Stream createActions() { - return facetedMethodsBuilder.getActionFacetedMethods() - .stream() + return factory.createActionFacetedMethods() .map(this::createAction) .filter(_NullSafe::isPresent); } - + // -- HELPER private ObjectAssociation createAssociation(final FacetedMethod facetMethod) { @@ -73,5 +71,5 @@ private ObjectAction createAction(final FacetedMethod facetedMethod) { } else return null; } - + } From 0d616ac8c67455e5d269c9874b56dc5af5877dea Mon Sep 17 00:00:00 2001 From: andi-huber Date: Thu, 30 Jul 2026 10:02:22 +0200 Subject: [PATCH 09/22] CAUSEWAY-4044: wip --- .../spec/impl/ObjectSpecificationDefault.java | 35 ++++------------- .../metamodel/spec/impl/PostProcessor.java | 24 ++++++------ .../spec/impl/RegularMemberFactory.java | 3 +- .../spec/impl/SpecificationPopulator.java | 39 +++++++++++++++++++ 4 files changed, 60 insertions(+), 41 deletions(-) create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationPopulator.java 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 7ce1d736dd1..9bfe3f88615 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 @@ -406,12 +406,6 @@ public Stream streamActionsForColumnRendering(final Where where) { return columnHelper.streamActionsForColumnRendering(this, where); } - - - //----------------------------------------------------------------------------------------------------------------- - // MERGED FROM FORMER ObjectSpecificationAbstract - //----------------------------------------------------------------------------------------------------------------- - // -- FIELDS private final PostProcessor postProcessor; @@ -485,28 +479,19 @@ public void introspect(final IntrospectionRequest request) { * @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)) { + if(introspectionState.isLessThan(upTo)) { introspectType(); } - if(isLessThan(upTo)) { - introspectFully(); - specLoaderInternal().validateLater(this, introspectionContextProvider); + if(introspectionState.isLessThan(upTo)) { + introspectFully(introspectionContextProvider); } } case TYPE_BEING_INTROSPECTED->{} // nothing to do (interim state during introspectType) case TYPE_INTROSPECTED->{ - if(isLessThan(upTo)) { - introspectFully(); - specLoaderInternal().validateLater(this, introspectionContextProvider); + if(introspectionState.isLessThan(upTo)) { + introspectFully(introspectionContextProvider); } } case MEMBERS_BEING_INTROSPECTED->{}// nothing to do (interim state during introspect fully) @@ -515,26 +500,20 @@ private void introspectUpTo(final IntrospectionState upTo, final Supplier introspectionContextProvider) { 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; + specLoaderInternal().validateLater(this, introspectionContextProvider); } protected void loadSpecOfSuperclass(final Class superclass) { 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/RegularMemberFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/RegularMemberFactory.java index e7276f039c7..0f7911e82a1 100644 --- 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 @@ -24,12 +24,11 @@ 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.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; record RegularMemberFactory( - ObjectSpecification spec, + ObjectSpecificationBuilder spec, FacetedMethodsFactory factory) { Stream createAssociations() { diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationPopulator.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationPopulator.java new file mode 100644 index 00000000000..4a176b05437 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationPopulator.java @@ -0,0 +1,39 @@ +/* + * 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.core.metamodel.services.classsubstitutor.ClassSubstitutor; +import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationBuilder.IntrospectionRequest; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +interface SpecificationPopulator { + + /** + * Return the specification for the specified class of object. + * + *

    It is possible for this method to return null, for example if + * any of the configured {@link ClassSubstitutor}s has filtered out the class. + * + * @return {@code null} if {@code domainType==null}, or if the type should be ignored. + */ + @Nullable + ObjectSpecificationBuilder getSpecificationBuilder(@Nullable Class domainType, @NonNull IntrospectionRequest request); + +} From ae95243668425a61aa44df76d3c80635877f61d7 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 31 Jul 2026 07:09:11 +0200 Subject: [PATCH 10/22] CAUSEWAY-4044: finding the spot when to exactly run createMixedInMembersAndResort --- .../spec/impl/ObjectSpecificationDefault.java | 19 +++++---- .../metamodel/spec/impl/_ValidateUtil.java | 42 +++++++++---------- 2 files changed, 32 insertions(+), 29 deletions(-) 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 9bfe3f88615..b360acc0aa5 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 @@ -27,6 +27,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; import java.util.function.Supplier; import java.util.stream.Stream; @@ -109,6 +110,7 @@ import org.apache.causeway.core.metamodel.util.Facets; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import lombok.Getter; @@ -259,6 +261,7 @@ private void introspectTypeHierarchy() { loadSpecOfInterfaces(getCorrespondingClass().getInterfaces()); } + private final AtomicBoolean isLockedDown = new AtomicBoolean(); //TODO temporary private void introspectMembers() { // yet this logic does not skip UNKNONW @@ -270,6 +273,8 @@ private void introspectMembers() { } return; } + Assert.isTrue(!isLockedDown.get(), ()->"object spec for '%s' is in lockdown, because postprocessing already had run (cannot run twice)" + .formatted(getCorrespondingClass().getName())); var memberFactory = new RegularMemberFactory(this, facetedMethodsFactory); @@ -277,8 +282,12 @@ private void introspectMembers() { replaceAssociations(memberFactory.createAssociations()); replaceActions(memberFactory.createActions()); + createMixedInMembersAndResort(); + postProcessor.postProcess(this); invalidateCachedFacets(); + + isLockedDown.set(true); } @Override @@ -579,7 +588,7 @@ protected void loadSpecOfInterfaces(final Class[] interfaces) { } } - protected final void replaceAssociations(final Stream associations) { + final void replaceAssociations(final Stream associations) { var orderedAssociations = _MemberSortingUtils.sortAssociationsIntoList(associations); synchronized (unmodifiableAssociations) { this.associations.clear(); @@ -588,7 +597,7 @@ protected final void replaceAssociations(final Stream associa } } - protected final void replaceActions(final Stream objectActions) { + final void replaceActions(final Stream objectActions) { var orderedActions = _MemberSortingUtils.sortActionsIntoList(objectActions); synchronized (unmodifiableActions){ this.objectActions.clear(); @@ -808,8 +817,6 @@ public Stream streamDeclaredAssociations(final MixedIn mixedI introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, ()->"streamDeclaredAssociations of %s".formatted(this.getFeatureIdentifier())); - mixedInMemberAdder.trigger(this::createMixedInMembersAndResort); // only if not already - synchronized(unmodifiableAssociations) { return stream(unmodifiableAssociations.get()) .filter(mixedIn.toFilter()); @@ -860,15 +867,11 @@ public Stream streamDeclaredActions( final MixedIn mixedIn) { introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, ()->"streamDeclaredActions of %s".formatted(this.getFeatureIdentifier())); - - mixedInMemberAdder.trigger(this::createMixedInMembersAndResort); - return actionScopes.stream() .flatMap(actionScope->stream(objectActionsByType.get(actionScope))) .filter(mixedIn.toFilter()); } - // -- VALIDITY @Override 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); } From 569b82e9175280db1ac327d33ac3c03761e2d502 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 31 Jul 2026 08:18:03 +0200 Subject: [PATCH 11/22] CAUSEWAY-4044: immutable member containment --- .../internal/collections/_Multimaps.java | 6 +- .../spec/feature/ObjectActionContainer.java | 6 +- .../metamodel/spec/impl/ActionContainer.java | 179 ++++++++++++++ .../spec/impl/AssociationContainer.java | 103 ++++++++ .../spec/impl/HasObjectActionContainer.java | 78 ++++++ .../impl/HasObjectAssociationContainer.java | 58 +++++ .../spec/impl/ObjectMemberContainer.java | 133 ---------- .../spec/impl/ObjectSpecificationBuilder.java | 5 +- .../spec/impl/ObjectSpecificationDefault.java | 230 ++++-------------- .../spec/impl/_MemberIdClashReporting.java | 12 +- ...rameterAbstractTest_getId_and_getName.java | 10 +- 11 files changed, 496 insertions(+), 324 deletions(-) create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ActionContainer.java create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/AssociationContainer.java create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasObjectActionContainer.java create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasObjectAssociationContainer.java delete mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectMemberContainer.java 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/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/impl/ActionContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ActionContainer.java new file mode 100644 index 00000000000..b84ec81f10f --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ActionContainer.java @@ -0,0 +1,179 @@ +/* + * 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 static org.apache.causeway.commons.internal.base._NullSafe.stream; + +import java.util.List; +import java.util.Map; +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.Can; +import org.apache.causeway.commons.collections.ImmutableEnumSet; +import org.apache.causeway.commons.internal.base._Strings; +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.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 actionScopes, + // partitions and caches objectActions by type; updated in sortCacheAndUpdateActions() + ListMultimap objectActionsByType, + Can actionsInOrder, + ObjectActionContainer superContainer) +implements ObjectActionContainer { + + // e.g. used for value types + static ActionContainer EMPTY = new ActionContainer( + ImmutableEnumSet.noneOf(ActionScope.class), + _Multimaps.newListMultimap(Map::of, List::of), + Can.empty(), + null); + + ActionContainer( + final List actionsInOrder, + /** + * scopes as available at runtime + */ + final ImmutableEnumSet actionScopes, + final ObjectActionContainer superContainer) { + this(actionScopes, _Multimaps.newListMultimap(), + build(actionsInOrder), + superContainer); + buildMap(); + } + + private static Can build(final List actionsInOrder) { + return Can.ofCollection(actionsInOrder); + } + + @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 = _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; + }); + } + + @Override + public Stream streamRuntimeActions(final MixedIn mixedIn) { + return streamActions(actionScopes, 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->stream(objectActionsByType.get(actionScope))) + .filter(mixedIn.toFilter()); + } + + // -- HELPER + + private boolean isTypeHierarchyRoot() { + return superContainer==null; + } + + private void buildMap() { + // rebuild objectActionsByType multi-map + for (var actionType : ActionScope.values()) { + var objectActionForType = objectActionsByType.getOrElseNew(actionType); + objectActionForType.clear(); + actionsInOrder.stream() + .filter(ObjectAction.Predicates.ofActionType(actionType)) + .forEach(objectActionForType::add); + } + } + +} 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..0efd3184814 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/AssociationContainer.java @@ -0,0 +1,103 @@ +/* + * 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.feature.MixedIn; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; +import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer; + +record AssociationContainer( + Can associationsInOrder, + ObjectAssociationContainer superContainer) +implements ObjectAssociationContainer { + + // e.g. used for value types + static AssociationContainer EMPTY = new AssociationContainer( + Can.empty(), + null); + + AssociationContainer( + final List associationsInOrder, + final ObjectAssociationContainer superContainer) { + this(Can.ofCollection(associationsInOrder), superContainer); + } + + @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) { + // TODO Auto-generated method stub + return null; + } + + @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/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/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 index 6a613192385..a1832be4348 100644 --- 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 @@ -22,11 +22,14 @@ 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 - ObjectMemberContainer, HasSpecificationLoaderInternal, + ObjectActionContainer, + ObjectAssociationContainer, ObjectSpecification // TODO remove // Specification, // HasLogicalType, 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 b360acc0aa5..724226334ed 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,10 +18,7 @@ */ package org.apache.causeway.core.metamodel.spec.impl; -import static org.apache.causeway.commons.internal.base._NullSafe.stream; - import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -37,20 +34,15 @@ 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._Lazy; -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.reflection._ClassCache; import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; @@ -109,7 +101,6 @@ import org.apache.causeway.core.metamodel.spi.EntityTitleSubscriber; import org.apache.causeway.core.metamodel.util.Facets; import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -119,7 +110,10 @@ @Slf4j final class ObjectSpecificationDefault -implements ObjectSpecificationBuilder { +implements + ObjectSpecificationBuilder, + HasObjectActionContainer, + HasObjectAssociationContainer { // -- CONSTRUCTION @@ -140,6 +134,11 @@ final class ObjectSpecificationDefault @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; + public ObjectSpecificationDefault( final @NonNull CausewayBeanMetaData typeMeta, final @NonNull FacetProcessor facetProcessor, @@ -183,8 +182,8 @@ public ObjectSpecificationRecord build() { getFeatureType(), facetHolder, this,//Hierarchical, - this,//ObjectActionContainer - this,//ObjectAssociationContainer + objectActionContainer, + objectAssociationContainer, getServiceRegistry().select(EntityTitleSubscriber.class), introspectionPolicy, aliases(), @@ -276,13 +275,29 @@ private void introspectMembers() { Assert.isTrue(!isLockedDown.get(), ()->"object spec for '%s' is in lockdown, because postprocessing already had run (cannot run twice)" .formatted(getCorrespondingClass().getName())); - var memberFactory = new RegularMemberFactory(this, facetedMethodsFactory); + // fully introspect up the type hierarchy including interfaces + // because members creation depends on presence of inherited members + streamTypeHierarchyAndInterfaces() + .forEach(it->((ObjectSpecificationDefault)it) + .introspect(IntrospectionRequest.FULL)); // create associations and actions - replaceAssociations(memberFactory.createAssociations()); - replaceActions(memberFactory.createActions()); - createMixedInMembersAndResort(); + var regularMemberFactory = new RegularMemberFactory(this, facetedMethodsFactory); + var regularAssociations = regularMemberFactory.createAssociations().toList(); + var regularActions = regularMemberFactory.createActions().toList(); + + var mixedInMemberFactory = new MixedInMemberFactory(this, specLoaderInternal()); + var mixedInAssociations = mixedInMemberFactory.createMixedInAssociations(); + var mixedInActions = mixedInMemberFactory.createMixedInActions(); + + this.objectAssociationContainer = new AssociationContainer( + associationsInOrder(regularAssociations, mixedInAssociations), + superclass()); + this.objectActionContainer = new ActionContainer( + actionsInOrder(regularActions, mixedInActions), + ActionScope.forEnvironment(getMetaModelContext().getSystemEnvironment()), + superclass()); postProcessor.postProcess(this); invalidateCachedFacets(); @@ -331,25 +346,6 @@ private void addNamedFacetIfRequired() { // -- 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, @@ -404,44 +400,15 @@ 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); - } - // -- FIELDS private final PostProcessor postProcessor; - // -- 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(); @@ -525,7 +492,7 @@ private void introspectFully(final Supplier introspectionContextProvider specLoaderInternal().validateLater(this, introspectionContextProvider); } - protected void loadSpecOfSuperclass(final Class superclass) { + private void loadSpecOfSuperclass(final Class superclass) { if (superclass == null) return; @@ -536,7 +503,7 @@ protected void loadSpecOfSuperclass(final Class superclass) { } } - protected void loadSpecOfInterfaces(final Class[] interfaces) { + private void loadSpecOfInterfaces(final Class[] interfaces) { if(interfaces==null) return; @@ -588,34 +555,25 @@ protected void loadSpecOfInterfaces(final Class[] interfaces) { } } - final void replaceAssociations(final Stream associations) { - var orderedAssociations = _MemberSortingUtils.sortAssociationsIntoList(associations); - synchronized (unmodifiableAssociations) { - this.associations.clear(); - this.associations.addAll(orderedAssociations); - unmodifiableAssociations.clear(); // invalidate - } + private List associationsInOrder( + final List regularAssociations, + final List mixedInAssociations) { + _MemberIdClashReporting.flagAnyMemberIdClashes(this, regularAssociations, mixedInAssociations); // do before sorting + return _MemberSortingUtils.sortAssociationsIntoList(Stream.concat( + regularAssociations.stream(), + mixedInAssociations.stream())); } - 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); - } - } + private List actionsInOrder( + final List regularActions, + final List mixedInActions) { + _MemberIdClashReporting.flagAnyMemberIdClashes(this, regularActions, mixedInActions); // do before sorting + return _MemberSortingUtils.sortActionsIntoList(Stream.concat( + regularActions.stream(), + mixedInActions.stream())); } - void invalidateCachedFacets() { + private void invalidateCachedFacets() { this.valueFacet = getFacet(ValueFacet.class); this.titleFacet = lookupNonFallbackFacet(TitleFacet.class).orElse(null); this.iconFacet = getFacet(IconFacet.class); @@ -810,19 +768,6 @@ public Can interfaces() { return unmodifiableInterfaces.get(); } - // -- ASSOCIATIONS - - @Override - public Stream streamDeclaredAssociations(final MixedIn mixedIn) { - introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, - ()->"streamDeclaredAssociations of %s".formatted(this.getFeatureIdentifier())); - - synchronized(unmodifiableAssociations) { - return stream(unmodifiableAssociations.get()) - .filter(mixedIn.toFilter()); - } - } - @Override public Optional getMember(final String memberId) { introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, @@ -842,36 +787,6 @@ 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())); - return actionScopes.stream() - .flatMap(actionScope->stream(objectActionsByType.get(actionScope))) - .filter(mixedIn.toFilter()); - } - // -- VALIDITY @Override @@ -902,49 +817,6 @@ public ObjectValidityContext createValidityInteractionContext( return new ObjectValidityContext(targetAdapter, getFeatureIdentifier(), interactionInitiatedBy); } - // -- MIXIN ADDER ONESHOTs - - private final _Oneshot mixedInMemberAdder = new _Oneshot(); - - /** - * one-shot: must be no-op, if already created - */ - private void createMixedInMembersAndResort() { - var memberFactory = new MixedInMemberFactory(this, specLoaderInternal()); - createMixedInActionsAndResort(memberFactory); - createMixedInAssociationsAndResort(memberFactory); - } - - private void createMixedInActionsAndResort(final MixedInMemberFactory memberFactory) { - var mixedInActions = memberFactory.createMixedInActions(); - if(mixedInActions.isEmpty()) - return; // nothing to do (this spec has no mixed-in actions, regular actions have already been added) - - var regularActions = new ArrayList<>(objectActions); // defensive copy - - // note: we are doing this before any member sorting - _MemberIdClashReporting.flagAnyMemberIdClashes(this, regularActions, mixedInActions); - - replaceActions(Stream.concat( - regularActions.stream(), - mixedInActions.stream())); - } - - private void createMixedInAssociationsAndResort(final MixedInMemberFactory memberFactory) { - var mixedInAssociations = memberFactory.createMixedInAssociations(); - if(mixedInAssociations.isEmpty()) - return; // nothing to do (this spec has no mixed-in associations, regular associations have already been added) - - var regularAssociations = new ArrayList<>(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 Can titleSubscribers = getServiceRegistry().select(EntityTitleSubscriber.class); @@ -954,4 +826,10 @@ public boolean isFullyIntrospected() { return this.introspectionState == IntrospectionState.FULLY_INTROSPECTED; } + @Override + public Stream streamAssociationsForColumnRendering(final ColumnQuery columnQuery) { + //TODO migrate + return columnHelper.streamAssociationsForColumnRendering(this, columnQuery); + } + } 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/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..f7300c4a836 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.ObjectSpecificationBuilder.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(); } From 38dc55594399b814a56f50efc78ff6d963674394 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 31 Jul 2026 16:26:57 +0200 Subject: [PATCH 12/22] CAUSEWAY-4044: intermediate cleanup --- .../metamodel/facetapi/HasFacetHolder.java | 1 + .../spec/impl/AssociationContainer.java | 18 ++++++++--- .../metamodel/spec/impl/MemberPopulator.java | 32 +------------------ .../spec/impl/ObjectSpecificationDefault.java | 12 ++----- .../spec/impl/_MembersAsColumns.java | 26 +++++++-------- 5 files changed, 29 insertions(+), 60 deletions(-) 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/spec/impl/AssociationContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/AssociationContainer.java index 0efd3184814..780489a2674 100644 --- 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 @@ -25,24 +25,30 @@ 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) + 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) { - this(Can.ofCollection(associationsInOrder), superContainer); + final ObjectAssociationContainer superContainer, + final ObjectSpecification correspondingSpec) { + this(Can.ofCollection(associationsInOrder), superContainer, correspondingSpec); } @Override @@ -84,8 +90,10 @@ public Optional getDeclaredAssociation(final String id, final @Override public Stream streamAssociationsForColumnRendering(final ColumnQuery columnQuery) { - // TODO Auto-generated method stub - return null; + if(correspondingSpec==null) + return Stream.empty(); + return new _MembersAsColumns(correspondingSpec.getMetaModelContext()) + .streamAssociationsForColumnRendering(correspondingSpec, columnQuery); } @Override diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator.java index 63ca75ce56d..cef69569b3d 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator.java @@ -18,13 +18,9 @@ */ package org.apache.causeway.core.metamodel.spec.impl; -import java.util.stream.Stream; - import org.apache.causeway.applib.id.LogicalType; -import org.apache.causeway.commons.collections.Can; -import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; -import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation; +//TODO wip interface MemberPopulator { enum IntrospectionState { @@ -55,31 +51,5 @@ boolean isLessThan(final IntrospectionState other) { return this.ordinal() < other.ordinal(); } } - - record ComputedMembers( - Can associationsInOrder, - Can actionsInOrder - //Map membersByMethod, - ) { - - ComputedMembers() { - this(Can.empty(), Can.empty()); - } - - ComputedMembers( - final Stream associations, - final Stream actions) { - this( - Can.ofCollection(_MemberSortingUtils.sortAssociationsIntoList(associations)), - Can.ofCollection(_MemberSortingUtils.sortActionsIntoList(actions))); - } - - ComputedMembers join(final ComputedMembers other) { - return new ComputedMembers( - Stream.concat(this.associationsInOrder.stream(), other.associationsInOrder.stream()), - Stream.concat(this.actionsInOrder.stream(), other.actionsInOrder.stream())); - } - - } } \ No newline at end of file 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 724226334ed..2a64e967336 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 @@ -124,7 +124,6 @@ final class ObjectSpecificationDefault private final FacetedMethodsFactory facetedMethodsFactory; private final ClassSubstitutorRegistry classSubstitutorRegistry; - private final _MembersAsColumns columnHelper; private final _Lazy isInjectableLazy; private final _Lazy isDomainServiceLazy; @@ -169,8 +168,6 @@ public ObjectSpecificationDefault( this.facetedMethodsFactory = new FacetedMethodsFactory(this, facetProcessor, classSubstitutorRegistry); - - this.columnHelper = new _MembersAsColumns(mmc); } // -- SHALLOW IMMUTABLE @@ -293,7 +290,8 @@ private void introspectMembers() { this.objectAssociationContainer = new AssociationContainer( associationsInOrder(regularAssociations, mixedInAssociations), - superclass()); + superclass(), + this); this.objectActionContainer = new ActionContainer( actionsInOrder(regularActions, mixedInActions), ActionScope.forEnvironment(getMetaModelContext().getSystemEnvironment()), @@ -826,10 +824,4 @@ public boolean isFullyIntrospected() { return this.introspectionState == IntrospectionState.FULLY_INTROSPECTED; } - @Override - public Stream streamAssociationsForColumnRendering(final ColumnQuery columnQuery) { - //TODO migrate - return columnHelper.streamAssociationsForColumnRendering(this, columnQuery); - } - } 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 From b42b233e142ea6644c9092be37f56491c62becd7 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 31 Jul 2026 18:20:25 +0200 Subject: [PATCH 13/22] CAUSEWAY-4044: thread-safe IntrospectionStateHandler --- .../spec/impl/FacetedMethodsFactory.java | 2 +- ...java => HasIntrospectionStateHandler.java} | 27 +++--- ...or.java => IntrospectionStateHandler.java} | 51 +++++++++- .../IntrospectionStateHandlerThreadSafe.java | 87 +++++++++++++++++ .../spec/impl/MixedInMemberFactory.java | 2 +- .../spec/impl/ObjectSpecificationBuilder.java | 18 +--- .../spec/impl/ObjectSpecificationDefault.java | 93 +++++-------------- .../spec/impl/SpecificationLoaderDefault.java | 11 +-- .../impl/SpecificationLoaderInternal.java | 7 +- .../IntrospectionState_comparable_Test.java | 2 +- ...rameterAbstractTest_getId_and_getName.java | 2 +- parent/pom.xml | 2 + 12 files changed, 187 insertions(+), 117 deletions(-) rename core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/{SpecificationPopulator.java => HasIntrospectionStateHandler.java} (52%) rename core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/{MemberPopulator.java => IntrospectionStateHandler.java} (53%) create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionStateHandlerThreadSafe.java diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsFactory.java index 256ad92f99d..975466c0718 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsFactory.java @@ -49,7 +49,7 @@ 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.ObjectSpecificationBuilder.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; diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationPopulator.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasIntrospectionStateHandler.java similarity index 52% rename from core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationPopulator.java rename to core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasIntrospectionStateHandler.java index 4a176b05437..264d4249680 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationPopulator.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/HasIntrospectionStateHandler.java @@ -18,22 +18,19 @@ */ package org.apache.causeway.core.metamodel.spec.impl; -import org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutor; -import org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationBuilder.IntrospectionRequest; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; +@FunctionalInterface +interface HasIntrospectionStateHandler extends IntrospectionStateHandler { -interface SpecificationPopulator { + IntrospectionStateHandler introspectionStateHandler(); - /** - * Return the specification for the specified class of object. - * - *

    It is possible for this method to return null, for example if - * any of the configured {@link ClassSubstitutor}s has filtered out the class. - * - * @return {@code null} if {@code domainType==null}, or if the type should be ignored. - */ - @Nullable - ObjectSpecificationBuilder getSpecificationBuilder(@Nullable Class domainType, @NonNull IntrospectionRequest request); + @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/MemberPopulator.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionStateHandler.java similarity index 53% rename from core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator.java rename to core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionStateHandler.java index cef69569b3d..1f669d37ea9 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MemberPopulator.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionStateHandler.java @@ -19,9 +19,9 @@ package org.apache.causeway.core.metamodel.spec.impl; import org.apache.causeway.applib.id.LogicalType; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; -//TODO wip -interface MemberPopulator { +interface IntrospectionStateHandler { enum IntrospectionState { /** @@ -52,4 +52,51 @@ boolean isLessThan(final IntrospectionState other) { } } + 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..914e4bcb007 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionStateHandlerThreadSafe.java @@ -0,0 +1,87 @@ +/* + * 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; + +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 + + // This ensures only one thread changes state at a time, + // but threads block while another thread holds the lock. + synchronized (lock) { + 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.state = IntrospectionState.TYPE_BEING_INTROSPECTED; + introspectTypeHierarchy.run(); + this.state = IntrospectionState.TYPE_INTROSPECTED; + } + + private void transitionToFullyIntrospected() { + 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/MixedInMemberFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixedInMemberFactory.java index ac1aecf86e0..527450e1139 100644 --- 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 @@ -29,7 +29,7 @@ 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.impl.ObjectSpecificationBuilder.IntrospectionRequest; +import org.apache.causeway.core.metamodel.spec.impl.IntrospectionStateHandler.IntrospectionRequest; record MixedInMemberFactory( ObjectSpecification spec, 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 index a1832be4348..dffa8443243 100644 --- 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 @@ -30,6 +30,7 @@ interface ObjectSpecificationBuilder HasSpecificationLoaderInternal, ObjectActionContainer, ObjectAssociationContainer, + IntrospectionStateHandler, ObjectSpecification // TODO remove // Specification, // HasLogicalType, @@ -41,25 +42,8 @@ interface ObjectSpecificationBuilder // HasSpecificationLoaderInternal { - 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 - } - ObjectSpecification build(); - void introspect(IntrospectionRequest request); Set getPotentialOrphans(); - boolean isFullyIntrospected(); } 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 2a64e967336..51981ed6d05 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 @@ -26,7 +26,6 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; -import java.util.function.Supplier; import java.util.stream.Stream; import org.apache.causeway.applib.Identifier; @@ -96,7 +95,6 @@ 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.spec.impl.MemberPopulator.IntrospectionState; 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; @@ -112,6 +110,7 @@ final class ObjectSpecificationDefault implements ObjectSpecificationBuilder, + HasIntrospectionStateHandler, HasObjectActionContainer, HasObjectAssociationContainer { @@ -127,6 +126,9 @@ final class ObjectSpecificationDefault private final _Lazy isInjectableLazy; private final _Lazy isDomainServiceLazy; + @Getter @Accessors(fluent = true) + private final IntrospectionStateHandler introspectionStateHandler; + @Getter(onMethod_={@Override}) private final IntrospectionPolicy introspectionPolicy; @@ -168,6 +170,18 @@ public ObjectSpecificationDefault( this.facetedMethodsFactory = new FacetedMethodsFactory(this, facetProcessor, classSubstitutorRegistry); + + this.introspectionStateHandler = new IntrospectionStateHandlerThreadSafe( + ()->{ + introspectTypeHierarchy(); + invalidateCachedFacets(); + }, + ()->{ + introspectMembers(); +// // make sure we've loaded the facets from layout.xml also. + Facets.gridPreload(this, null); + specLoaderInternal().validateLater(this); + }); } // -- SHALLOW IMMUTABLE @@ -275,8 +289,8 @@ private void introspectMembers() { // fully introspect up the type hierarchy including interfaces // because members creation depends on presence of inherited members streamTypeHierarchyAndInterfaces() - .forEach(it->((ObjectSpecificationDefault)it) - .introspect(IntrospectionRequest.FULL)); + .forEach(it->((IntrospectionStateHandler)it) + .introspectFully()); // create associations and actions @@ -303,6 +317,7 @@ private void introspectMembers() { isLockedDown.set(true); } +<<<<<<< Upstream, based on origin/main @Override public void synthesizeNavigationActions() { if (!getMetaModelContext().getConfiguration() @@ -334,6 +349,10 @@ public void synthesizeNavigationActions() { membersByMethod = null; } +======= + //TODO this is a facet factory responsibility + @Deprecated +>>>>>>> df432ff CAUSEWAY-4044: thread-safe IntrospectionStateHandler private void addNamedFacetIfRequired() { if (getFacet(MemberNamedFacet.class) == null) { addFacet(new MemberNamedFacetForStaticMemberName( @@ -346,8 +365,7 @@ private void addNamedFacetIfRequired() { @Override public Optional getMember(final ResolvedMethod method) { - introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, - ()->"getMember %s on %s".formatted(method.name(), this.getFeatureIdentifier())); + introspectFully(); if (membersByMethod == null) { this.membersByMethod = catalogueMembers(); @@ -427,8 +445,6 @@ public Optional explicitElementSpec() { private AliasedFacet aliasedFacet; private CssClassFacet cssClassFacet; - private IntrospectionState introspectionState = IntrospectionState.NOT_INTROSPECTED; - @Getter(onMethod_ = {@Override}) private final FacetHolder facetHolder; // -- Stuff immediately derivable from class @@ -437,59 +453,6 @@ public final FeatureType getFeatureType() { return FeatureType.OBJECT; } - @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)); - } - } - - /** - * @param introspectionContextProvider keeps track of the causal chain of introspection requests - */ - private void introspectUpTo(final IntrospectionState upTo, final Supplier introspectionContextProvider) { - switch (introspectionState) { - case NOT_INTROSPECTED->{ - if(introspectionState.isLessThan(upTo)) { - introspectType(); - } - if(introspectionState.isLessThan(upTo)) { - introspectFully(introspectionContextProvider); - } - } - case TYPE_BEING_INTROSPECTED->{} // nothing to do (interim state during introspectType) - case TYPE_INTROSPECTED->{ - if(introspectionState.isLessThan(upTo)) { - introspectFully(introspectionContextProvider); - } - } - case MEMBERS_BEING_INTROSPECTED->{}// nothing to do (interim state during introspect fully) - case FULLY_INTROSPECTED->{}// nothing to do ... all done - } - } - - private void introspectType() { - this.introspectionState = IntrospectionState.TYPE_BEING_INTROSPECTED; - introspectTypeHierarchy(); - invalidateCachedFacets(); - this.introspectionState = IntrospectionState.TYPE_INTROSPECTED; - } - - private void introspectFully(final Supplier introspectionContextProvider) { - 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); - specLoaderInternal().validateLater(this, introspectionContextProvider); - } - private void loadSpecOfSuperclass(final Class superclass) { if (superclass == null) return; @@ -768,8 +731,7 @@ public Can interfaces() { @Override public Optional getMember(final String memberId) { - introspectUpTo(IntrospectionState.FULLY_INTROSPECTED, - ()->"getMember %s of %s".formatted(memberId, this.getFeatureIdentifier())); + introspectionStateHandler.introspectFully(); if(_Strings.isEmpty(memberId)) return Optional.empty(); @@ -819,9 +781,4 @@ public ObjectValidityContext createValidityInteractionContext( private final Can titleSubscribers = getServiceRegistry().select(EntityTitleSubscriber.class); - @Override - public boolean isFullyIntrospected() { - return this.introspectionState == IntrospectionState.FULLY_INTROSPECTED; - } - } 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 25c70824c72..cd9a89707a4 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,7 +31,6 @@ 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 org.apache.causeway.applib.Identifier; @@ -67,7 +66,7 @@ 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.ObjectSpecificationBuilder.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; @@ -386,8 +385,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 @@ -397,7 +395,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 @@ -557,7 +555,8 @@ private ObjectSpecificationBuilder loadSpecificationNullable( 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 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 339baa0a11b..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,7 +19,6 @@ package org.apache.causeway.core.metamodel.spec.impl; import java.util.Optional; -import java.util.function.Supplier; import org.apache.causeway.applib.id.LogicalType; import org.apache.causeway.applib.services.bookmark.Bookmark; @@ -28,7 +27,7 @@ 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.ObjectSpecificationBuilder.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; @@ -157,9 +156,7 @@ default Optional lookupBeanSort(final @Nullable LogicalType logicalTyp /** * queue {@code objectSpec} for later validation - * @param objectSpec - * @param introspectionContextProvider */ - void validateLater(ObjectSpecification objectSpec, Supplier introspectionContextProvider); + void validateLater(ObjectSpecification objectSpec); } 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 6a5eb3deedb..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 @@ -21,7 +21,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import org.apache.causeway.core.metamodel.spec.impl.MemberPopulator.IntrospectionState; +import org.apache.causeway.core.metamodel.spec.impl.IntrospectionStateHandler.IntrospectionState; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; 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 f7300c4a836..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 @@ -26,7 +26,7 @@ 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.ObjectSpecificationBuilder.IntrospectionRequest; +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; 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 From a81ba5fa8c16974001b3592cdfc678ff49527156 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 31 Jul 2026 19:00:12 +0200 Subject: [PATCH 14/22] CAUSEWAY-4044: fixes potential deadlock in prev commit --- .../IntrospectionStateHandlerThreadSafe.java | 68 +++++++++++-------- 1 file changed, 39 insertions(+), 29 deletions(-) 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 index 914e4bcb007..ed89a8870da 100644 --- 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 @@ -18,6 +18,9 @@ */ package org.apache.causeway.core.metamodel.spec.impl; +/** + * Guarantees thread-safe state transition. + */ final class IntrospectionStateHandlerThreadSafe implements IntrospectionStateHandler { @@ -45,43 +48,50 @@ public void introspectUpTo(final IntrospectionState upTo) { if(isFullyIntrospected()) return; // optimization - // This ensures only one thread changes state at a time, - // but threads block while another thread holds the lock. - synchronized (lock) { - 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 - } - } + 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.state = IntrospectionState.TYPE_BEING_INTROSPECTED; - introspectTypeHierarchy.run(); - this.state = IntrospectionState.TYPE_INTROSPECTED; + // 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.state = IntrospectionState.MEMBERS_BEING_INTROSPECTED; - introspectMembers.run(); - this.state = IntrospectionState.FULLY_INTROSPECTED; + // 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 From 3594f23a7d7f988ee41839d33c3901182d59e5f9 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sat, 1 Aug 2026 07:38:57 +0200 Subject: [PATCH 15/22] CAUSEWAY-4044: wip --- .../commons/internal/debug/_Debug.java | 69 +++++++++++++++++-- .../spec/impl/MixedInMemberFactory.java | 33 +++------ .../spec/impl/MixinSpecStreamer.java | 32 +++++++++ .../spec/impl/MixinSpecStreamerEager.java | 44 ++++++++++++ .../spec/impl/MixinSpecStreamerOnTheFly.java | 38 ++++++++++ .../spec/impl/ObjectSpecificationDefault.java | 45 ++++++++---- .../spec/impl/SpecificationLoaderDefault.java | 27 ++++++-- 7 files changed, 240 insertions(+), 48 deletions(-) create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamer.java create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamerEager.java create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamerOnTheFly.java 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..8f0aa1b8259 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,6 +18,11 @@ */ package org.apache.causeway.commons.internal.debug; +import java.util.HashMap; +import java.util.LongSummaryStatistics; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.causeway.commons.internal.base._NullSafe; @@ -46,7 +51,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 +61,79 @@ 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: %d ms, avg %.2f ms (count=%d)" + .formatted(name, + stats.getSum()/1000_000L, + stats.getAverage()/1000_000., + stats.getCount()); + } + } + + public Profiler() { + this(new HashMap<>()); + } + + 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")); + } + } + // -- HELPER private void dump(Object x, final int indent) { 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 index 527450e1139..edb5a08967c 100644 --- 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 @@ -23,27 +23,17 @@ import java.util.function.Function; import java.util.stream.Stream; -import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry; 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; -import org.apache.causeway.core.metamodel.spec.impl.IntrospectionStateHandler.IntrospectionRequest; record MixedInMemberFactory( ObjectSpecification spec, - SpecificationLoaderInternal specLoaderInternal, - CausewayBeanTypeRegistry causewayBeanTypeRegistry) { + MixinSpecStreamer mixinSpecStreamer) { - MixedInMemberFactory( - final ObjectSpecification spec, - final SpecificationLoaderInternal specLoaderInternal) { - this(spec, specLoaderInternal, spec.getServiceRegistry() - .lookupServiceElseFail(CausewayBeanTypeRegistry.class)); - } - - /** + /** * Creates all mixed in properties and collections for this spec. */ public List createMixedInAssociations() { @@ -51,7 +41,7 @@ public List createMixedInAssociations() { && !spec.isInjectable() && !spec.isValue(); return include - ? causewayBeanTypeRegistry.streamMixinTypes() + ? mixinSpecStreamer.streamMixinSpecs() .flatMap(this::createMixedInAssociation) .toList() : List.of(); @@ -66,7 +56,7 @@ public List createMixedInActions() { // in support of composite value-type constructor mixins || spec.beanSort().isValue(); return include - ? causewayBeanTypeRegistry.streamMixinTypes() + ? mixinSpecStreamer.streamMixinSpecs() .flatMap(this::createMixedInAction) .toList() : List.of(); @@ -74,11 +64,8 @@ public List createMixedInActions() { // -- HELPER - private Stream createMixedInAssociation(final Class mixinType) { - var mixinSpec = specLoaderInternal.loadSpecification(mixinType, - IntrospectionRequest.FULL); - if (mixinSpec == null - || mixinSpec == spec) + private Stream createMixedInAssociation(final ObjectSpecification mixinSpec) { + if (mixinSpec == spec) return Stream.empty(); var mixinFacet = mixinSpec.mixinFacet().orElse(null); if(mixinFacet == null) @@ -94,12 +81,8 @@ private Stream createMixedInAssociation(final Class mixinT .map(mixedInAssociation(spec, mixinSpec, mixinMethodName)); } - private Stream createMixedInAction(final Class mixinType) { - - var mixinSpec = specLoaderInternal.loadSpecification(mixinType, - IntrospectionRequest.FULL); - if (mixinSpec == null - || mixinSpec == spec) + private Stream createMixedInAction(final ObjectSpecification mixinSpec) { + if (mixinSpec == spec) return Stream.empty(); var mixinFacet = mixinSpec.mixinFacet().orElse(null); if(mixinFacet == null) diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamer.java new file mode 100644 index 00000000000..eeff2a2ad74 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamer.java @@ -0,0 +1,32 @@ +/* + * 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.core.metamodel.spec.ObjectSpecification; + +@FunctionalInterface +interface MixinSpecStreamer { + + static MixinSpecStreamer EMPTY = Stream::empty; + + Stream streamMixinSpecs(); + +} 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..4dc30731b15 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamerEager.java @@ -0,0 +1,44 @@ +/* + * 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.core.config.beans.CausewayBeanTypeRegistry; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; + +record MixinSpecStreamerEager(Can mixinSpecs) +implements MixinSpecStreamer { + + MixinSpecStreamerEager(final SpecificationLoader specLoader, final Can> mixinTypes) { + this(mixinTypes.map(specLoader::specForTypeElseFail)); + } + + MixinSpecStreamerEager(final SpecificationLoader specLoader, final CausewayBeanTypeRegistry beanTypeRegistry) { + this(specLoader, beanTypeRegistry.streamMixinTypes().collect(Can.toCan())); + } + + @Override + public Stream streamMixinSpecs() { + return mixinSpecs.stream(); + } + +} 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..cd150b673f6 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamerOnTheFly.java @@ -0,0 +1,38 @@ +/* + * 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.core.config.beans.CausewayBeanTypeRegistry; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; + +record MixinSpecStreamerOnTheFly( + SpecificationLoader specLoader, + CausewayBeanTypeRegistry beanTypeRegistry) +implements MixinSpecStreamer { + + @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/ObjectSpecificationDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java index 51981ed6d05..aff4e5ecd19 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 @@ -43,11 +43,13 @@ import org.apache.causeway.commons.internal.collections._Lists; import org.apache.causeway.commons.internal.collections._Maps; import org.apache.causeway.commons.internal.collections._Sets; +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; @@ -141,10 +143,12 @@ final class ObjectSpecificationDefault private ActionContainer objectActionContainer = ActionContainer.EMPTY; public ObjectSpecificationDefault( + final Profiler profiler, final @NonNull CausewayBeanMetaData typeMeta, final @NonNull FacetProcessor facetProcessor, final @NonNull PostProcessor postProcessor, - final @NonNull ClassSubstitutorRegistry classSubstitutorRegistry) { + final @NonNull ClassSubstitutorRegistry classSubstitutorRegistry, + final @NonNull MixinSpecStreamer mixinSpecStreamer) { final MetaModelContext mmc = facetProcessor.getMetaModelContext(); @@ -173,13 +177,16 @@ public ObjectSpecificationDefault( this.introspectionStateHandler = new IntrospectionStateHandlerThreadSafe( ()->{ - introspectTypeHierarchy(); + profiler.measure("types", this::introspectTypeHierarchy); + //introspectTypeHierarchy(); invalidateCachedFacets(); }, ()->{ - introspectMembers(); + profiler.measure("members", ()->introspectMembers(mixinSpecStreamer, profiler)); + //introspectMembers(); // // make sure we've loaded the facets from layout.xml also. - Facets.gridPreload(this, null); + //Facets.gridPreload(this, null); + profiler.measure("gridPreload", ()->Facets.gridPreload(this, null)); specLoaderInternal().validateLater(this); }); } @@ -272,7 +279,7 @@ private void introspectTypeHierarchy() { } private final AtomicBoolean isLockedDown = new AtomicBoolean(); //TODO temporary - private void introspectMembers() { + private void introspectMembers(final MixinSpecStreamer mixinSpecStreamer, final Profiler profiler) { // yet this logic does not skip UNKNONW if(this.beanSort().isCollection() @@ -288,19 +295,25 @@ private void introspectMembers() { // fully introspect up the type hierarchy including interfaces // because members creation depends on presence of inherited members - streamTypeHierarchyAndInterfaces() - .forEach(it->((IntrospectionStateHandler)it) - .introspectFully()); + + profiler.measure("hierarchy", ()->{ + streamTypeHierarchyAndInterfaces() + .forEach(it->((IntrospectionStateHandler)it) + .introspectFully()); + }); // create associations and actions var regularMemberFactory = new RegularMemberFactory(this, facetedMethodsFactory); - var regularAssociations = regularMemberFactory.createAssociations().toList(); - var regularActions = regularMemberFactory.createActions().toList(); + var regularAssociations = profiler.measure("-regularAssociations", ()->regularMemberFactory.createAssociations().toList()); + var regularActions = profiler.measure("-regularActions", ()->regularMemberFactory.createActions().toList()); - var mixedInMemberFactory = new MixedInMemberFactory(this, specLoaderInternal()); - var mixedInAssociations = mixedInMemberFactory.createMixedInAssociations(); - var mixedInActions = mixedInMemberFactory.createMixedInActions(); + var mixinSpecStreamerX = new MixinSpecStreamerOnTheFly( + specLoaderInternal(), getServiceRegistry().lookupServiceElseFail(CausewayBeanTypeRegistry.class)); + var mixedInMemberFactory = new MixedInMemberFactory(this, mixinSpecStreamerX); + //XXX takes 50% of time + var mixedInAssociations = profiler.measure("-mixedInAssociations", ()->mixedInMemberFactory.createMixedInAssociations()); + var mixedInActions = profiler.measure("-mixedInActions", ()->mixedInMemberFactory.createMixedInActions()); this.objectAssociationContainer = new AssociationContainer( associationsInOrder(regularAssociations, mixedInAssociations), @@ -311,7 +324,11 @@ private void introspectMembers() { ActionScope.forEnvironment(getMetaModelContext().getSystemEnvironment()), superclass()); - postProcessor.postProcess(this); + profiler.measure("-postProcessor", ()->{ + //XXX takes 50% of time + postProcessor.postProcess(this); + }); + invalidateCachedFacets(); isLockedDown.set(true); 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 cd9a89707a4..3d2fa1b2ede 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 @@ -46,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; @@ -119,6 +120,8 @@ class SpecificationLoaderDefault private final Provider valueSemanticsResolver; private final ProgrammingModel programmingModel; private PostProcessor postProcessor; + private MixinSpecStreamer mixinSpecStreamer = MixinSpecStreamer.EMPTY; + private final Profiler profiler = new Profiler(); @Inject public List preloadableTypes = Collections.emptyList(); @@ -238,6 +241,7 @@ public void createMetaModel() { this.facetProcessor = new FacetProcessor(programmingModel); this.postProcessor = new PostProcessor(programmingModel); + var specs = new SpecCollector(); // preload otherwise not eagerly discovered classes @@ -262,8 +266,14 @@ public void createMetaModel() { .forEach(specs::collect); introspectAndLog("type hierarchies", specs.knownSpecs, IntrospectionRequest.TYPE_ONLY); + //this.mixinSpecStreamer = new MixinSpecStreamerOnTheFly(this, causewayBeanTypeRegistry); introspectAndLog("value types", specs.valueSpecs.values(), IntrospectionRequest.FULL); + //this.mixinSpecStreamer = MixinSpecStreamer.EMPTY; introspectAndLog("mixins", specs.mixinSpecs, IntrospectionRequest.FULL); + + // lockdown + this.mixinSpecStreamer = new MixinSpecStreamerEager(this, causewayBeanTypeRegistry); + introspectAndLog("domain services", specs.domainServiceSpecs, IntrospectionRequest.FULL); introspectAndLog("entities (%s)".formatted(causewayBeanTypeRegistry.persistenceStack().name()), specs.entitySpecs(), IntrospectionRequest.FULL); @@ -293,8 +303,11 @@ public void createMetaModel() { if(isFullIntrospect()) { setMetamodelFullyIntrospected(true); } + + log.info("\n{}", profiler); } + @Override public Optional getValidationResult() { return validationResult.getMemoized(); @@ -487,6 +500,7 @@ public void addValidationFailure(final ValidationFailure validationFailure) { private final AtomicBoolean validationInProgress = new AtomicBoolean(false); private final BlockingQueue validationQueue = new LinkedBlockingQueue<>(); + //private Can mixinSpecs = Can.empty(); private ValidationFailures runMetaModelValidators() { validationInProgress.set(true); @@ -592,12 +606,15 @@ private ObjectSpecificationBuilder loadSpecificationNullable( /** * Creates the appropriate type of {@link ObjectSpecification}. */ - private ObjectSpecificationBuilder createSpecification(final CausewayBeanMetaData typeMeta) { + private ObjectSpecificationBuilder createSpecification( + final CausewayBeanMetaData typeMeta) { var objectSpec = new ObjectSpecificationDefault( - typeMeta, - facetProcessor, - postProcessor, - classSubstitutorRegistry); + profiler, + typeMeta, + facetProcessor, + postProcessor, + classSubstitutorRegistry, + mixinSpecStreamer); return objectSpec; } From 90d44c0f5806735d803c5386e4235fea012969dd Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sat, 1 Aug 2026 10:12:47 +0200 Subject: [PATCH 16/22] CAUSEWAY-4044: typo --- .../commons/internal/debug/_Debug.java | 50 +++++++++++-- .../MemberDescribedFacetFromType.java | 10 +-- .../all/DescribedAsFromTypePostProcessor.java | 30 ++++---- .../metamodel/spec/impl/ActionContainer.java | 70 +++++++++---------- .../spec/impl/MixedInMemberFactory.java | 39 ++++++----- .../spec/impl/MixinSpecStreamerEager.java | 8 +-- .../spec/impl/ObjectSpecificationDefault.java | 10 +-- .../spec/impl/SpecificationLoaderDefault.java | 11 ++- 8 files changed, 127 insertions(+), 101 deletions(-) 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 8f0aa1b8259..defe07f7eae 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,10 +18,10 @@ */ package org.apache.causeway.commons.internal.debug; -import java.util.HashMap; 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; @@ -104,16 +104,16 @@ T collect(final Supplier callable) { } @Override public final String toString() { - return "Profiling %s: %d ms, avg %.2f ms (count=%d)" + return "Profiling %s: %.3f ms, avg %.3f ms (count=%d)" .formatted(name, - stats.getSum()/1000_000L, + (stats.getSum())/1000_000., stats.getAverage()/1000_000., stats.getCount()); } } public Profiler() { - this(new HashMap<>()); + this(new ConcurrentHashMap<>()); } public void measure(final String name, final Runnable runnable) { @@ -129,11 +129,49 @@ public T measure(final String name, final Supplier callable) { @Override public final String toString() { return new TreeMap<>(measurements).values().stream() - .map(Measurement::toString) - .collect(Collectors.joining("\n")); + .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/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/postprocessors/all/DescribedAsFromTypePostProcessor.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/DescribedAsFromTypePostProcessor.java index 7db6f25ac6e..a30538a48cb 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,6 +33,8 @@ 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 { @@ -66,27 +66,25 @@ public void postProcessCollection(final ObjectSpecification objectSpecification, // -- 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/spec/impl/ActionContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ActionContainer.java index b84ec81f10f..ae1efee7a20 100644 --- 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 @@ -18,21 +18,15 @@ */ package org.apache.causeway.core.metamodel.spec.impl; -import static org.apache.causeway.commons.internal.base._NullSafe.stream; - +import java.util.HashSet; import java.util.List; -import java.util.Map; 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.Can; import org.apache.causeway.commons.collections.ImmutableEnumSet; import org.apache.causeway.commons.internal.base._Strings; -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.core.metamodel.spec.ActionScope; import org.apache.causeway.core.metamodel.spec.feature.MixedIn; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; @@ -42,18 +36,16 @@ record ActionContainer( /** * scopes as available at runtime */ - ImmutableEnumSet actionScopes, - // partitions and caches objectActions by type; updated in sortCacheAndUpdateActions() - ListMultimap objectActionsByType, - Can actionsInOrder, + ImmutableEnumSet actionScopesAtRuntime, + List productionActions, + List prototypeActions, ObjectActionContainer superContainer) implements ObjectActionContainer { - // e.g. used for value types + // useful types that have no mixin support e.g. value types static ActionContainer EMPTY = new ActionContainer( ImmutableEnumSet.noneOf(ActionScope.class), - _Multimaps.newListMultimap(Map::of, List::of), - Can.empty(), + List.of(), List.of(), //Can.empty(), null); ActionContainer( @@ -63,18 +55,15 @@ record ActionContainer( */ final ImmutableEnumSet actionScopes, final ObjectActionContainer superContainer) { - this(actionScopes, _Multimaps.newListMultimap(), - build(actionsInOrder), + this(actionScopes, + catalogue(actionsInOrder, ActionScope.PRODUCTION), + catalogue(actionsInOrder, ActionScope.PROTOTYPE), superContainer); - buildMap(); - } - - private static Can build(final List actionsInOrder) { - return Can.ofCollection(actionsInOrder); } @Override - public Optional getAction(final String id, + public Optional getAction( + final String id, final ImmutableEnumSet actionScopes, final MixedIn mixedIn) { var declaredAction = getDeclaredAction(id, mixedIn); // no inheritance nor type considered @@ -92,7 +81,9 @@ public Optional getAction(final String id, } @Override - public Optional getDeclaredAction(final String id, final ImmutableEnumSet actionScopes, + public Optional getDeclaredAction( + final String id, + final ImmutableEnumSet actionScopes, final MixedIn mixedIn) { return _Strings.isEmpty(id) ? Optional.empty() @@ -105,7 +96,9 @@ public Optional getDeclaredAction(final String id, final Immutable } @Override - public Stream streamActions(final ImmutableEnumSet actionTypes, final MixedIn mixedIn, + public Stream streamActions( + final ImmutableEnumSet actionTypes, + final MixedIn mixedIn, final Consumer onActionOverloaded) { var actionStream = isTypeHierarchyRoot() @@ -114,8 +107,8 @@ public Stream streamActions(final ImmutableEnumSet ac streamDeclaredActions(actionTypes, mixedIn), superContainer.streamActions(actionTypes, mixedIn)); - var actionSignatures = _Sets.newHashSet(); - var actionIds = _Sets.newHashSet(); + var actionSignatures = new HashSet(); + var actionIds = new HashSet(); return actionStream @@ -140,7 +133,7 @@ public Stream streamActions(final ImmutableEnumSet ac @Override public Stream streamRuntimeActions(final MixedIn mixedIn) { - return streamActions(actionScopes, mixedIn); + return streamActions(actionScopesAtRuntime, mixedIn); } @Override @@ -155,7 +148,7 @@ public Stream streamDeclaredActions( final ImmutableEnumSet actionScopes, final MixedIn mixedIn) { return actionScopes.stream() - .flatMap(actionScope->stream(objectActionsByType.get(actionScope))) + .flatMap(actionScope->list(actionScope).stream()) .filter(mixedIn.toFilter()); } @@ -165,15 +158,18 @@ private boolean isTypeHierarchyRoot() { return superContainer==null; } - private void buildMap() { - // rebuild objectActionsByType multi-map - for (var actionType : ActionScope.values()) { - var objectActionForType = objectActionsByType.getOrElseNew(actionType); - objectActionForType.clear(); - actionsInOrder.stream() - .filter(ObjectAction.Predicates.ofActionType(actionType)) - .forEach(objectActionForType::add); - } + 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/MixedInMemberFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixedInMemberFactory.java index edb5a08967c..49895f466e8 100644 --- 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 @@ -23,6 +23,7 @@ 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; @@ -35,15 +36,25 @@ record MixedInMemberFactory( /** * Creates all mixed in properties and collections for this spec. + * @param profiler */ - public List createMixedInAssociations() { - var include = spec.isEntityOrViewModelOrAbstract() + public List createMixedInAssociations(final Profiler profiler) { + + var include = + + profiler.measure("members.mixedInAssociations.createMixedInAssociation.inclusion", ()-> + + spec.isEntityOrViewModelOrAbstract() && !spec.isInjectable() - && !spec.isValue(); + && !spec.isValue() + ); + return include - ? mixinSpecStreamer.streamMixinSpecs() + ? profiler.measure("members.mixedInAssociations.createMixedInAssociation.stream", ()-> + mixinSpecStreamer.streamMixinSpecs() + .filter(mixinSpec-> mixinSpec != spec) .flatMap(this::createMixedInAssociation) - .toList() + .toList()) : List.of(); } @@ -57,6 +68,7 @@ public List createMixedInActions() { || spec.beanSort().isValue(); return include ? mixinSpecStreamer.streamMixinSpecs() + .filter(mixinSpec-> mixinSpec != spec) .flatMap(this::createMixedInAction) .toList() : List.of(); @@ -65,25 +77,19 @@ public List createMixedInActions() { // -- HELPER private Stream createMixedInAssociation(final ObjectSpecification mixinSpec) { - if (mixinSpec == spec) - return Stream.empty(); - var mixinFacet = mixinSpec.mixinFacet().orElse(null); + 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(spec.getCorrespondingClass())) return Stream.empty(); - var mixinMethodName = mixinFacet.getMainMethodName(); - return mixinSpec.streamActions(ActionScope.ANY, MixedIn.EXCLUDED) .filter(_SpecPredicates::isMixedInAssociation) .map(ObjectActionDefault.class::cast) - .map(mixedInAssociation(spec, mixinSpec, mixinMethodName)); + .map(mixedInAssociation(spec, mixinSpec, mixinFacet.getMainMethodName())); } private Stream createMixedInAction(final ObjectSpecification mixinSpec) { - if (mixinSpec == spec) - return Stream.empty(); var mixinFacet = mixinSpec.mixinFacet().orElse(null); if(mixinFacet == null) // this shouldn't happen; to be covered by meta-model validation later @@ -95,14 +101,12 @@ private Stream createMixedInAction(final ObjectSpecificatio && 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(mixedInAction(spec, mixinSpec, mixinMethodName)); + .map(mixedInAction(spec, mixinSpec, mixinFacet.getMainMethodName())); } /** @@ -131,8 +135,7 @@ private static Function mixedInAssociati final ObjectSpecification mixinSpec, final String mixinMethodName) { - return mixinAction -> - mixinAction.getReturnType().isSingular() + return mixinAction -> mixinAction.getReturnType().isSingular() ? new OneToOneAssociationMixedIn( mixeeSpec, mixinAction, mixinSpec, mixinMethodName) : new OneToManyAssociationMixedIn( 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 index 4dc30731b15..acb8751541a 100644 --- 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 @@ -28,12 +28,10 @@ record MixinSpecStreamerEager(Can mixinSpecs) implements MixinSpecStreamer { - MixinSpecStreamerEager(final SpecificationLoader specLoader, final Can> mixinTypes) { - this(mixinTypes.map(specLoader::specForTypeElseFail)); - } - MixinSpecStreamerEager(final SpecificationLoader specLoader, final CausewayBeanTypeRegistry beanTypeRegistry) { - this(specLoader, beanTypeRegistry.streamMixinTypes().collect(Can.toCan())); + this(beanTypeRegistry.streamMixinTypes() + .map(specLoader::specForTypeElseFail) + .collect(Can.toCan())); } @Override 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 aff4e5ecd19..b5de51afee2 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 @@ -305,15 +305,15 @@ private void introspectMembers(final MixinSpecStreamer mixinSpecStreamer, final // create associations and actions var regularMemberFactory = new RegularMemberFactory(this, facetedMethodsFactory); - var regularAssociations = profiler.measure("-regularAssociations", ()->regularMemberFactory.createAssociations().toList()); - var regularActions = profiler.measure("-regularActions", ()->regularMemberFactory.createActions().toList()); + var regularAssociations = profiler.measure("members.regularAssociations", ()->regularMemberFactory.createAssociations().toList()); + var regularActions = profiler.measure("members.regularActions", ()->regularMemberFactory.createActions().toList()); var mixinSpecStreamerX = new MixinSpecStreamerOnTheFly( specLoaderInternal(), getServiceRegistry().lookupServiceElseFail(CausewayBeanTypeRegistry.class)); var mixedInMemberFactory = new MixedInMemberFactory(this, mixinSpecStreamerX); //XXX takes 50% of time - var mixedInAssociations = profiler.measure("-mixedInAssociations", ()->mixedInMemberFactory.createMixedInAssociations()); - var mixedInActions = profiler.measure("-mixedInActions", ()->mixedInMemberFactory.createMixedInActions()); + var mixedInAssociations = profiler.measure("members.mixedInAssociations", ()->mixedInMemberFactory.createMixedInAssociations(profiler)); + var mixedInActions = profiler.measure("members.mixedInActions", ()->mixedInMemberFactory.createMixedInActions()); this.objectAssociationContainer = new AssociationContainer( associationsInOrder(regularAssociations, mixedInAssociations), @@ -324,7 +324,7 @@ private void introspectMembers(final MixinSpecStreamer mixinSpecStreamer, final ActionScope.forEnvironment(getMetaModelContext().getSystemEnvironment()), superclass()); - profiler.measure("-postProcessor", ()->{ + profiler.measure("members.postProcessor", ()->{ //XXX takes 50% of time postProcessor.postProcess(this); }); 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 3d2fa1b2ede..5c3586c0aa9 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 @@ -241,16 +241,15 @@ public void createMetaModel() { this.facetProcessor = new FacetProcessor(programmingModel); this.postProcessor = new PostProcessor(programmingModel); - 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)) @@ -266,12 +265,10 @@ public void createMetaModel() { .forEach(specs::collect); introspectAndLog("type hierarchies", specs.knownSpecs, IntrospectionRequest.TYPE_ONLY); - //this.mixinSpecStreamer = new MixinSpecStreamerOnTheFly(this, causewayBeanTypeRegistry); introspectAndLog("value types", specs.valueSpecs.values(), IntrospectionRequest.FULL); - //this.mixinSpecStreamer = MixinSpecStreamer.EMPTY; introspectAndLog("mixins", specs.mixinSpecs, IntrospectionRequest.FULL); - // lockdown + // lock down mixins this.mixinSpecStreamer = new MixinSpecStreamerEager(this, causewayBeanTypeRegistry); introspectAndLog("domain services", specs.domainServiceSpecs, IntrospectionRequest.FULL); @@ -284,7 +281,7 @@ public void createMetaModel() { if(isFullIntrospect()) { var snapshot = snapshotSpecifications(); log.info(" - introspecting all {} types eagerly (FullIntrospect=true)", snapshot.size()); - introspect(snapshot.filter(x->x.beanSort().isMixin()), IntrospectionRequest.FULL); + //introspect(snapshot.filter(x->x.beanSort().isMixin()), IntrospectionRequest.FULL); introspect(snapshot.filter(x->!x.beanSort().isMixin()), IntrospectionRequest.FULL); } From 018082aafc7581f129b01626b873d31376b87830 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Mon, 3 Aug 2026 12:03:35 +0200 Subject: [PATCH 17/22] CAUSEWAY-4044: intermediate clean up --- .../services/factory/FactoryService.java | 6 +- .../concurrent/_ConcurrentTaskList.java | 16 +++-- .../commons/internal/debug/_Debug.java | 6 ++ .../beans/CausewayBeanTypeRegistry.java | 4 +- .../CausewaySystemEnvironment.java | 24 +++---- .../facets/object/mixin/MixinFacet.java | 5 +- .../object/mixin/MixinFacetAbstract.java | 34 ++++----- .../all/MixinSanityChecksValidator.java | 2 +- .../services/init/MetamodelInitializer.java | 13 ++-- .../spec/impl/MixedInMemberFactory.java | 32 +++------ .../spec/impl/MixinSpecStreamer.java | 8 +++ .../spec/impl/MixinSpecStreamerEager.java | 23 +++++- .../spec/impl/MixinSpecStreamerOnTheFly.java | 10 ++- .../spec/impl/ObjectSpecificationDefault.java | 15 ++-- .../spec/impl/SpecificationLoaderDefault.java | 25 +++++-- .../specloader/SpecificationLoader.java | 7 +- .../factory/FactoryServiceDefault.java | 70 +++++++++---------- ...sViewer_IntegTest.dump_facets.approved.xml | 2 +- ...nDomain_IntegTest.dump_facets.approved.xml | 2 +- ...etaModelRegressionTest.verify.approved.xml | 48 ++++++------- 20 files changed, 197 insertions(+), 155 deletions(-) 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/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 defe07f7eae..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 @@ -27,7 +27,9 @@ 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; @@ -112,6 +114,10 @@ public final String toString() { } } + public static Profiler getInstance() { + return _Context.computeIfAbsent(Profiler.class, (@NonNull Supplier) Profiler::new); + } + public Profiler() { this(new ConcurrentHashMap<>()); } 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/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 7acd65cdd64..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 @@ -66,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/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/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/spec/impl/MixedInMemberFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixedInMemberFactory.java index 49895f466e8..423b4883fe9 100644 --- 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 @@ -45,15 +45,14 @@ public List createMixedInAssociations(final Profiler profiler profiler.measure("members.mixedInAssociations.createMixedInAssociation.inclusion", ()-> spec.isEntityOrViewModelOrAbstract() - && !spec.isInjectable() && !spec.isValue() + && !spec.isInjectable() ); return include ? profiler.measure("members.mixedInAssociations.createMixedInAssociation.stream", ()-> - mixinSpecStreamer.streamMixinSpecs() - .filter(mixinSpec-> mixinSpec != spec) - .flatMap(this::createMixedInAssociation) + mixinSpecStreamer.streamMixinSpecsFor(spec) + .flatMap(it->createMixedInAssociation(it, profiler)) .toList()) : List.of(); } @@ -67,8 +66,7 @@ public List createMixedInActions() { // in support of composite value-type constructor mixins || spec.beanSort().isValue(); return include - ? mixinSpecStreamer.streamMixinSpecs() - .filter(mixinSpec-> mixinSpec != spec) + ? mixinSpecStreamer.streamMixinSpecsFor(spec) .flatMap(this::createMixedInAction) .toList() : List.of(); @@ -76,26 +74,18 @@ public List createMixedInActions() { // -- HELPER - private Stream createMixedInAssociation(final ObjectSpecification mixinSpec) { - 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(spec.getCorrespondingClass())) - return Stream.empty(); + 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.getMainMethodName())); + .map(mixedInAssociation(spec, mixinSpec, mixinFacet.mainMethodName())); + }); } private Stream createMixedInAction(final ObjectSpecification mixinSpec) { - 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(spec.getCorrespondingClass())) - return Stream.empty(); + var mixinFacet = mixinSpec.mixinFacetElseFail(); // don't mixin Object_ mixins to domain services if(spec.beanSort().isManagedBeanContributing() && mixinFacet.isMixinFor(java.lang.Object.class)) @@ -106,7 +96,7 @@ private Stream createMixedInAction(final ObjectSpecificatio .filter(this::whenIsValueThenIsAlsoConstructorMixin) .filter(_SpecPredicates::isMixedInAction) .map(ObjectActionDefault.class::cast) - .map(mixedInAction(spec, mixinSpec, mixinFacet.getMainMethodName())); + .map(mixedInAction(spec, mixinSpec, mixinFacet.mainMethodName())); } /** diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamer.java index eeff2a2ad74..8950b98914f 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamer.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/MixinSpecStreamer.java @@ -29,4 +29,12 @@ interface MixinSpecStreamer { Stream streamMixinSpecs(); + 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 index acb8751541a..2f4014b13e4 100644 --- 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 @@ -21,17 +21,29 @@ 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) +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) - .collect(Can.toCan())); + .filter(mixinSpec-> mixinSpec.mixinFacet().isPresent()) + .collect(Can.toCan()), + _Multimaps.newListMultimap(), + Profiler.getInstance()); + streamMixinSpecs() + .forEach(mixinSpec-> + mixinsByMixeeClass.putElement(mixinSpec.mixinFacetElseFail().mixeeType(), mixinSpec)); } @Override @@ -39,4 +51,11 @@ 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 index cd150b673f6..135d77ebfa0 100644 --- 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 @@ -20,15 +20,23 @@ 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; record MixinSpecStreamerOnTheFly( SpecificationLoader specLoader, - CausewayBeanTypeRegistry beanTypeRegistry) + CausewayBeanTypeRegistry beanTypeRegistry, + Profiler profiler) implements MixinSpecStreamer { + MixinSpecStreamerOnTheFly( + final SpecificationLoader specLoader, + final CausewayBeanTypeRegistry beanTypeRegistry) { + this(specLoader, beanTypeRegistry, Profiler.getInstance()); + } + @Override public Stream streamMixinSpecs() { return beanTypeRegistry.streamMixinTypes() 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 b5de51afee2..2edd86175f3 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 @@ -26,6 +26,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; +import java.util.function.Supplier; import java.util.stream.Stream; import org.apache.causeway.applib.Identifier; @@ -49,7 +50,6 @@ 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; @@ -143,12 +143,11 @@ final class ObjectSpecificationDefault private ActionContainer objectActionContainer = ActionContainer.EMPTY; public ObjectSpecificationDefault( - final Profiler profiler, final @NonNull CausewayBeanMetaData typeMeta, final @NonNull FacetProcessor facetProcessor, final @NonNull PostProcessor postProcessor, final @NonNull ClassSubstitutorRegistry classSubstitutorRegistry, - final @NonNull MixinSpecStreamer mixinSpecStreamer) { + final @NonNull Supplier mixinSpecStreamerSupplier) { final MetaModelContext mmc = facetProcessor.getMetaModelContext(); @@ -175,6 +174,8 @@ public ObjectSpecificationDefault( this.facetedMethodsFactory = new FacetedMethodsFactory(this, facetProcessor, classSubstitutorRegistry); + var profiler = Profiler.getInstance(); + this.introspectionStateHandler = new IntrospectionStateHandlerThreadSafe( ()->{ profiler.measure("types", this::introspectTypeHierarchy); @@ -182,7 +183,7 @@ public ObjectSpecificationDefault( invalidateCachedFacets(); }, ()->{ - profiler.measure("members", ()->introspectMembers(mixinSpecStreamer, profiler)); + profiler.measure("members", ()->introspectMembers(mixinSpecStreamerSupplier.get(), profiler)); //introspectMembers(); // // make sure we've loaded the facets from layout.xml also. //Facets.gridPreload(this, null); @@ -308,10 +309,7 @@ private void introspectMembers(final MixinSpecStreamer mixinSpecStreamer, final var regularAssociations = profiler.measure("members.regularAssociations", ()->regularMemberFactory.createAssociations().toList()); var regularActions = profiler.measure("members.regularActions", ()->regularMemberFactory.createActions().toList()); - var mixinSpecStreamerX = new MixinSpecStreamerOnTheFly( - specLoaderInternal(), getServiceRegistry().lookupServiceElseFail(CausewayBeanTypeRegistry.class)); - var mixedInMemberFactory = new MixedInMemberFactory(this, mixinSpecStreamerX); - //XXX takes 50% of time + var mixedInMemberFactory = new MixedInMemberFactory(this, mixinSpecStreamer); var mixedInAssociations = profiler.measure("members.mixedInAssociations", ()->mixedInMemberFactory.createMixedInAssociations(profiler)); var mixedInActions = profiler.measure("members.mixedInActions", ()->mixedInMemberFactory.createMixedInActions()); @@ -325,7 +323,6 @@ private void introspectMembers(final MixinSpecStreamer mixinSpecStreamer, final superclass()); profiler.measure("members.postProcessor", ()->{ - //XXX takes 50% of time postProcessor.postProcess(this); }); 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 5c3586c0aa9..531f9ec6a57 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 @@ -121,7 +121,7 @@ class SpecificationLoaderDefault private final ProgrammingModel programmingModel; private PostProcessor postProcessor; private MixinSpecStreamer mixinSpecStreamer = MixinSpecStreamer.EMPTY; - private final Profiler profiler = new Profiler(); + private final Profiler profiler = Profiler.getInstance(); @Inject public List preloadableTypes = Collections.emptyList(); @@ -227,6 +227,13 @@ public void collect(final @Nullable ObjectSpecificationBuilder spec) { } } + @Override + public boolean contains(@Nullable final Class cls) { + return cls!=null + ? cache.containsKey(cls) + : false; + } + /** * Initializes and wires up, and primes the cache based on any service * classes (provided by the {@link CausewayBeanTypeRegistry}). @@ -266,8 +273,9 @@ public void createMetaModel() { introspectAndLog("type hierarchies", specs.knownSpecs, IntrospectionRequest.TYPE_ONLY); introspectAndLog("value types", specs.valueSpecs.values(), IntrospectionRequest.FULL); - introspectAndLog("mixins", specs.mixinSpecs, IntrospectionRequest.FULL); + this.mixinSpecStreamer = new MixinSpecStreamerOnTheFly(this, causewayBeanTypeRegistry); + introspectAndLog("mixins", specs.mixinSpecs, IntrospectionRequest.FULL); // lock down mixins this.mixinSpecStreamer = new MixinSpecStreamerEager(this, causewayBeanTypeRegistry); @@ -281,7 +289,15 @@ public void createMetaModel() { if(isFullIntrospect()) { var snapshot = snapshotSpecifications(); log.info(" - introspecting all {} types eagerly (FullIntrospect=true)", snapshot.size()); - //introspect(snapshot.filter(x->x.beanSort().isMixin()), IntrospectionRequest.FULL); + snapshot.stream() + .filter(it->((ObjectSpecificationDefault)it).isFullyIntrospected()) + .forEach(it->{ + log.warn("not fully introspected after first pass {}", it); +// Assert.isTrue( +// +// ()->"not fully introspected %s".formatted(it)); + }); + introspect(snapshot.filter(x->x.beanSort().isMixin()), IntrospectionRequest.FULL); introspect(snapshot.filter(x->!x.beanSort().isMixin()), IntrospectionRequest.FULL); } @@ -606,12 +622,11 @@ private ObjectSpecificationBuilder loadSpecificationNullable( private ObjectSpecificationBuilder createSpecification( final CausewayBeanMetaData typeMeta) { var objectSpec = new ObjectSpecificationDefault( - profiler, typeMeta, facetProcessor, postProcessor, classSubstitutorRegistry, - mixinSpecStreamer); + ()->mixinSpecStreamer); return objectSpec; } 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/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/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/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..c7f2a1261bf 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 @@ - + @@ -6757,8 +6757,8 @@ - + @@ -8359,8 +8359,8 @@ - + @@ -34042,8 +34042,8 @@ - + @@ -34367,8 +34367,8 @@ - + @@ -34681,8 +34681,8 @@ - + @@ -35009,8 +35009,8 @@ - + @@ -35337,8 +35337,8 @@ - + @@ -35524,8 +35524,8 @@ - + @@ -35844,8 +35844,8 @@ - + @@ -36066,8 +36066,8 @@ - + @@ -36288,8 +36288,8 @@ - + @@ -36489,8 +36489,8 @@ - + @@ -36720,8 +36720,8 @@ - + @@ -36951,8 +36951,8 @@ - + @@ -39203,8 +39203,8 @@ - + @@ -39381,8 +39381,8 @@ - + @@ -39559,8 +39559,8 @@ - + @@ -39737,8 +39737,8 @@ - + @@ -39915,8 +39915,8 @@ - + @@ -40093,8 +40093,8 @@ - + @@ -40271,8 +40271,8 @@ - + @@ -40449,8 +40449,8 @@ - + @@ -49826,8 +49826,8 @@ - + From 2907f4a2a6842fd7c40eff3c7a00d784e1ab40e4 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Tue, 4 Aug 2026 09:23:59 +0200 Subject: [PATCH 18/22] CAUSEWAY-4044: proper warnings on mixin discovery that was too late --- .../all/DescribedAsFromTypePostProcessor.java | 10 +- .../services/metamodel/MetaModelExporter.java | 12 +- .../metamodel/MetaModelServiceDefault.java | 3 +- .../spec/impl/MixinSpecStreamerOnTheFly.java | 3 + .../spec/impl/ObjectSpecificationDefault.java | 12 +- .../spec/impl/SpecificationLoaderDefault.java | 49 +- .../jpa/conf/Configuration_usingJpa.java | 13 +- .../jpa/entities/JpaBook_delete.java | 4 +- .../composite/CalendarEventJaxbVm.java | 14 +- ...etaModelRegressionTest.verify.approved.xml | 1124 ----------------- 10 files changed, 74 insertions(+), 1170 deletions(-) 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 a30538a48cb..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 @@ -40,26 +40,26 @@ public class DescribedAsFromTypePostProcessor @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); } 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 9cb80f14124..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,13 +75,14 @@ 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) + if(useTinyDomain + && !tinyDomain.isEmpty()) return exportTinyDomain(tinyDomain, config); MetamodelDto metamodelDto = new MetamodelDto(); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java index bff57be6c11..e8534f3d7fd 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/metamodel/MetaModelServiceDefault.java @@ -18,6 +18,7 @@ */ package org.apache.causeway.core.metamodel.services.metamodel; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -128,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; 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 index 135d77ebfa0..9c4b5752987 100644 --- 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 @@ -25,18 +25,21 @@ 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() 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 2edd86175f3..d25886ec483 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 @@ -309,7 +309,9 @@ private void introspectMembers(final MixinSpecStreamer mixinSpecStreamer, final var regularAssociations = profiler.measure("members.regularAssociations", ()->regularMemberFactory.createAssociations().toList()); var regularActions = profiler.measure("members.regularActions", ()->regularMemberFactory.createActions().toList()); - var mixedInMemberFactory = new MixedInMemberFactory(this, mixinSpecStreamer); + 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()); @@ -322,9 +324,11 @@ private void introspectMembers(final MixinSpecStreamer mixinSpecStreamer, final ActionScope.forEnvironment(getMetaModelContext().getSystemEnvironment()), superclass()); - profiler.measure("members.postProcessor", ()->{ - postProcessor.postProcess(this); - }); + //TODO would allow to introspect mixins in isolation if(!isMixin()) { + profiler.measure("members.postProcessor", ()->{ + postProcessor.postProcess(this); + }); + //} invalidateCachedFacets(); 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 531f9ec6a57..66160c57275 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 @@ -273,12 +273,31 @@ public void createMetaModel() { introspectAndLog("type hierarchies", specs.knownSpecs, IntrospectionRequest.TYPE_ONLY); introspectAndLog("value types", specs.valueSpecs.values(), IntrospectionRequest.FULL); - - this.mixinSpecStreamer = new MixinSpecStreamerOnTheFly(this, causewayBeanTypeRegistry); + //this.mixinSpecStreamer = MixinSpecStreamer.EMPTY; + //this.mixinSpecStreamer = new MixinSpecStreamerOnTheFly(this, causewayBeanTypeRegistry); introspectAndLog("mixins", specs.mixinSpecs, IntrospectionRequest.FULL); - // lock down mixins + // lock down mixins, also assuming non of the previously fully introspected types need any mixins this.mixinSpecStreamer = new MixinSpecStreamerEager(this, causewayBeanTypeRegistry); + //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()); + }); + +//debug +// var mmService = new MetaModelServiceDefault(()->this, GridService.NOOP); +// var dto = mmService.getDomainModel(); +// System.err.println(YamlUtils.toStringUtf8(dto, JsonUtils::onlyIncludeNonNull)); + introspectAndLog("domain services", specs.domainServiceSpecs, IntrospectionRequest.FULL); introspectAndLog("entities (%s)".formatted(causewayBeanTypeRegistry.persistenceStack().name()), specs.entitySpecs(), IntrospectionRequest.FULL); @@ -286,19 +305,19 @@ public void createMetaModel() { serviceRegistry.lookupServiceElseFail(MenuBarsService.class).menuBars(); + var snapshot = snapshotSpecifications(); + snapshot.stream() + .filter(spec->spec.beanSort().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()) { - var snapshot = snapshotSpecifications(); - log.info(" - introspecting all {} types eagerly (FullIntrospect=true)", snapshot.size()); - snapshot.stream() - .filter(it->((ObjectSpecificationDefault)it).isFullyIntrospected()) - .forEach(it->{ - log.warn("not fully introspected after first pass {}", it); -// Assert.isTrue( -// -// ()->"not fully introspected %s".formatted(it)); - }); - introspect(snapshot.filter(x->x.beanSort().isMixin()), IntrospectionRequest.FULL); - introspect(snapshot.filter(x->!x.beanSort().isMixin()), IntrospectionRequest.FULL); + log.info(" - introspecting types not initially discovered by Spring {}", snapshot.size()); + introspect(snapshot.filter(spec->!spec.beanSort().isMixin()), IntrospectionRequest.FULL); } log.info(" - running remaining validators"); 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/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/MetaModelRegressionTest.verify.approved.xml b/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.verify.approved.xml index c7f2a1261bf..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 @@ -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 - - From 7d4aac1b5ab478cf75e500c3a7c5f80a08d48947 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Wed, 5 Aug 2026 20:21:16 +0200 Subject: [PATCH 19/22] CAUSEWAY-4044: fixes the issue of specs being fully introspected too early --- .../core/metamodel/object/Mm2YamlUtils.java | 137 ++++++++++++++++++ .../spec/impl/SpecificationLoaderDefault.java | 41 ++++-- 2 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/Mm2YamlUtils.java 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/spec/impl/SpecificationLoaderDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderDefault.java index 66160c57275..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 @@ -61,6 +61,7 @@ import org.apache.causeway.core.metamodel.commons.ClassUtil; 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; @@ -234,6 +235,14 @@ public boolean contains(@Nullable final Class 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}). @@ -275,9 +284,11 @@ public void createMetaModel() { 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); - // lock down mixins, also assuming non of the previously fully introspected types need any mixins + // 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 @@ -289,16 +300,11 @@ public void createMetaModel() { .forEach(spec->{ log.warn("type (non-mixin, non-value) found fully introspected after mixin introspection {}" + " - reload triggered", spec.getCorrespondingClass()); - invalidateCache(spec.getCorrespondingClass()); + //invalidateCache(spec.getCorrespondingClass()); //reloadSpecification(spec.getCorrespondingClass()); }); -//debug -// var mmService = new MetaModelServiceDefault(()->this, GridService.NOOP); -// var dto = mmService.getDomainModel(); -// System.err.println(YamlUtils.toStringUtf8(dto, JsonUtils::onlyIncludeNonNull)); - - introspectAndLog("domain services", specs.domainServiceSpecs, IntrospectionRequest.FULL); + 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); @@ -307,7 +313,7 @@ public void createMetaModel() { var snapshot = snapshotSpecifications(); snapshot.stream() - .filter(spec->spec.beanSort().isMixin()) + .filter(ObjectSpecificationBuilder::isMixin) .filter(spec->!spec.isFullyIntrospected()) .forEach(spec->{ log.warn("Mixin was missing during first pass {}." @@ -315,11 +321,15 @@ public void createMetaModel() { + "make sure it is discovered by Spring.", spec); }); - if(isFullIntrospect()) { + //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.beanSort().isMixin()), IntrospectionRequest.FULL); + 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 @@ -614,7 +624,14 @@ private ObjectSpecificationBuilder 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.aliases().isNotEmpty() // this bool. expr. is an optimization, not strictly required ... a bit of hack though From 5f707dad86752293cefe6c11ec057ec3826aa2e0 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Thu, 6 Aug 2026 09:42:44 +0200 Subject: [PATCH 20/22] CAUSEWAY-4044: integrating CAUSEWAY-3910 --- ...thesizeNavigationActionsPostProcessor.java | 56 ------------- .../spec/impl/ObjectSpecificationDefault.java | 58 ++++--------- .../spec/impl/ObjectSpecificationMutable.java | 48 ----------- .../spec/impl/ProgrammingModelDefault.java | 4 - .../spec/impl/SpecLoadingDevNotes.adoc | 53 ++++++++++++ .../SyntheticNavigationActionFactory.java | 83 +++++++++++-------- .../impl/SyntheticNavigationActionTest.java | 36 ++++---- ...CommandExecutorInteractionAdvisorTest.java | 23 +++-- .../MemberExecutorServiceDefaultTest.java | 19 ++--- 9 files changed, 151 insertions(+), 229 deletions(-) delete mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/members/navigation/SynthesizeNavigationActionsPostProcessor.java delete mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationMutable.java create mode 100644 core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecLoadingDevNotes.adoc 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/spec/impl/ObjectSpecificationDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java index d25886ec483..c829834f0d8 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,7 +18,6 @@ */ 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; @@ -44,6 +43,7 @@ import org.apache.causeway.commons.internal.collections._Lists; import org.apache.causeway.commons.internal.collections._Maps; 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; @@ -58,8 +58,6 @@ import org.apache.causeway.core.metamodel.facetapi.FacetHolder; import org.apache.causeway.core.metamodel.facetapi.FeatureType; 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.hide.HiddenFacet; @@ -315,12 +313,16 @@ private void introspectMembers(final MixinSpecStreamer mixinSpecStreamer, final 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( associationsInOrder(regularAssociations, mixedInAssociations), superclass(), this); this.objectActionContainer = new ActionContainer( - actionsInOrder(regularActions, mixedInActions), + actionsInOrder(regularActions, mixedInActions, syntheticActions), ActionScope.forEnvironment(getMetaModelContext().getSystemEnvironment()), superclass()); @@ -335,42 +337,8 @@ private void introspectMembers(final MixinSpecStreamer mixinSpecStreamer, final isLockedDown.set(true); } -<<<<<<< Upstream, based on origin/main - @Override - public void synthesizeNavigationActions() { - if (!getMetaModelContext().getConfiguration() - .extensions().commandLog().recordingSupport().isEnabled()) { - return; - } - - 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; - } - - replaceActions(Stream.concat(objectActions.stream(), syntheticActions.stream())); - membersByMethod = null; - } - -======= //TODO this is a facet factory responsibility @Deprecated ->>>>>>> df432ff CAUSEWAY-4044: thread-safe IntrospectionStateHandler private void addNamedFacetIfRequired() { if (getFacet(MemberNamedFacet.class) == null) { addFacet(new MemberNamedFacetForStaticMemberName( @@ -535,7 +503,7 @@ private void loadSpecOfInterfaces(final Class[] interfaces) { } private List associationsInOrder( - final List regularAssociations, + final List regularAssociations, final List mixedInAssociations) { _MemberIdClashReporting.flagAnyMemberIdClashes(this, regularAssociations, mixedInAssociations); // do before sorting return _MemberSortingUtils.sortAssociationsIntoList(Stream.concat( @@ -544,12 +512,14 @@ private List associationsInOrder( } private List actionsInOrder( - final List regularActions, - final List mixedInActions) { + final List regularActions, + final List mixedInActions, + final List syntheticActions) { _MemberIdClashReporting.flagAnyMemberIdClashes(this, regularActions, mixedInActions); // do before sorting - return _MemberSortingUtils.sortActionsIntoList(Stream.concat( - regularActions.stream(), - mixedInActions.stream())); + return _MemberSortingUtils.sortActionsIntoList(_Streams.concat( + regularActions.stream(), + mixedInActions.stream(), + syntheticActions.stream())); } private void invalidateCachedFacets() { 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/ObjectSpecificationMutable.java deleted file mode 100644 index b4e1561d27a..00000000000 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationMutable.java +++ /dev/null @@ -1,48 +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 org.apache.causeway.core.metamodel.spec.ObjectSpecification; - -//renamed -public interface ObjectSpecificationMutable extends ObjectSpecification { - - 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 introspect(IntrospectionRequest request); - - /** - * Adds configuration-gated framework navigation actions during metamodel post-processing. - */ - void synthesizeNavigationActions(); - -} 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/SpecLoadingDevNotes.adoc b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecLoadingDevNotes.adoc new file mode 100644 index 00000000000..e10c4b02756 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecLoadingDevNotes.adoc @@ -0,0 +1,53 @@ += 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 +- ClassSubstitutorRegistry/ClassSubstitutor + +[source,java] +.SpecificationLoader +---- +... +---- + +[source,java] +.ObjectSpecification +---- +... +---- + +== Building the Metamodel + +Given categories of classes (mixins, values, entities, viewmodels, ...) +the _Metamodel_ is bootstrapped. + +The internal `SpecificationLoader` shall hold a lookup map, +containing either a builder or a finished 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/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/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/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); From 084ec83c68d58ded762929dcd257eb0b1969aac9 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Thu, 6 Aug 2026 14:33:07 +0200 Subject: [PATCH 21/22] CAUSEWAY-4044: dev notes --- .../core/metamodel/spec/Hierarchical.java | 5 +-- .../spec/impl/SpecLoadingDevNotes.adoc | 42 ++++++++++++++++--- 2 files changed, 39 insertions(+), 8 deletions(-) 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 38cda693baf..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 @@ -22,7 +22,6 @@ import java.util.function.Predicate; import java.util.stream.Stream; -import org.apache.causeway.applib.annotation.Domain; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.internal.base._NullSafe; import org.apache.causeway.commons.internal.collections._Streams; @@ -80,7 +79,7 @@ static Optional lookupFacet(final Class facetType, 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 + // while the test method is called, collects the first occurrence of a fallback facet class FallbackFacetFilter implements Predicate { Q fallback; @@ -96,7 +95,7 @@ public boolean test(final Q facet) { return false; } } - + var filter = new FallbackFacetFilter(); return Optional.ofNullable(facetsCombined 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 index e10c4b02756..8514c700b75 100644 --- 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 @@ -8,27 +8,59 @@ - ObjectSpecification - FacetedMethodsBuilder - FacetProcessor +- PostProcessor - ClassSubstitutorRegistry/ClassSubstitutor [source,java] .SpecificationLoader ---- -... +ObjectSpecification loadSpecification(Class cls) ---- [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. +the _Metamodel_ is bootstrapped. -The internal `SpecificationLoader` shall hold a lookup map, -containing either a builder or a finished spec. +. 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 evicting a Spec from the cache. (In support of reloading a Spec) +We need to be sure none of the other Specs still holds a reference to the evicted Spec. + +=== Implementation + +The `ObjectSpecification`, as cached by the `SpecificationLoader`, +is a wrapper that has a stable identity, +but delegates to another internal instance of ObjectSpec (probably as sub-interface).  +On reload, we throw away the old internal instance, and rebuild a new one (eagerly or lazily as required). + [plantuml,fig-introspection-participants,svg] .Introspection Participants From 560e1c0a75018096e87083ba9bdbbc6dfb7f8591 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Fri, 7 Aug 2026 11:41:34 +0200 Subject: [PATCH 22/22] CAUSEWAY-4044: dev notes refined --- .../spec/impl/SpecLoadingDevNotes.adoc | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) 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 index 8514c700b75..2c05811937c 100644 --- 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 @@ -11,12 +11,28 @@ - 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 ---- @@ -51,16 +67,9 @@ However TYPE_ONLY introspection is allowed. .. 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 evicting a Spec from the cache. (In support of reloading a Spec) -We need to be sure none of the other Specs still holds a reference to the evicted Spec. - -=== Implementation - -The `ObjectSpecification`, as cached by the `SpecificationLoader`, -is a wrapper that has a stable identity, -but delegates to another internal instance of ObjectSpec (probably as sub-interface).  -On reload, we throw away the old internal instance, and rebuild a new one (eagerly or lazily as required). - +. 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