You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2026/06/03 14:28:33 UTC
(causeway) branch 4005-valuesem.qualifier created (now 97e998f810d)
This is an automated email from the ASF dual-hosted git repository.
andi-huber pushed a change to branch 4005-valuesem.qualifier
in repository https://gitbox.apache.org/repos/asf/causeway.git
at 97e998f810d CAUSEWAY-4005: adds support for bean method provided qualifier detection
This branch includes the following new commits:
new 97e998f810d CAUSEWAY-4005: adds support for bean method provided qualifier detection
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(causeway) 01/01: CAUSEWAY-4005: adds support for bean method provided qualifier detection
Posted by ah...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
andi-huber pushed a commit to branch 4005-valuesem.qualifier
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 97e998f810d558090e2851513daa238ed0c44df5
Author: andi-huber <ah...@apache.org>
AuthorDate: Wed Jun 3 16:28:20 2026 +0200
CAUSEWAY-4005: adds support for bean method provided qualifier detection
---
.../value/semantics/ValueSemanticsResolver.java | 10 +--
.../internal/ioc/SingletonBeanProvider.java | 1 -
.../commons/internal/ioc/SpringContextHolder.java | 27 +++----
.../commons/internal/reflection/_ClassCache.java | 44 +++++++++--
.../config/beans/CausewayBeanTypeClassifier.java | 60 ++++++---------
.../RemoveAnnotatedMethodsFacetFactory.java | 3 +-
.../facets/object/value/ValueFacetAbstract.java | 86 ++++++++++------------
...ionOrAnyMatchingValueSemanticsFacetFactory.java | 5 +-
.../vsp/ValueFacetUsingSemanticsProvider.java | 2 +-
.../services/registry/ServiceRegistryDefault.java | 49 +++++++++++-
.../schema/SchemaValueMarshallerAbstract.java | 21 +++---
.../causeway/core/metamodel/util/Facets.java | 12 +++
.../valuetypes/ValueSemanticsResolverDefault.java | 28 ++++---
.../test/components/scalars/ConverterTester.java | 10 +--
14 files changed, 207 insertions(+), 151 deletions(-)
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/ValueSemanticsResolver.java b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/ValueSemanticsResolver.java
index 520f063b2b7..ca593c3d6ea 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/ValueSemanticsResolver.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/ValueSemanticsResolver.java
@@ -20,10 +20,7 @@
import java.util.stream.Stream;
-import org.apache.causeway.applib.Identifier;
-import org.apache.causeway.commons.collections.Can;
-
-import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* @since 2.0 {@index}
@@ -33,11 +30,8 @@ public interface ValueSemanticsResolver {
boolean hasValueSemantics(Class<?> valueType);
<T> Stream<ValueSemanticsProvider<T>> streamValueSemantics(Class<T> valueType);
+ <T> Stream<ValueSemanticsProvider<T>> streamValueSemantics(Class<T> valueType, @Nullable String requiredQualifier);
Stream<Class<?>> streamClassesWithValueSemantics();
- <T> Can<ValueSemanticsProvider<T>> selectValueSemantics(
- @NonNull Identifier featureIdentifier,
- @NonNull Class<T> valueType);
-
}
diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/ioc/SingletonBeanProvider.java b/commons/src/main/java/org/apache/causeway/commons/internal/ioc/SingletonBeanProvider.java
index fe945ca20f3..9883c9428c7 100644
--- a/commons/src/main/java/org/apache/causeway/commons/internal/ioc/SingletonBeanProvider.java
+++ b/commons/src/main/java/org/apache/causeway/commons/internal/ioc/SingletonBeanProvider.java
@@ -45,7 +45,6 @@ public record SingletonBeanProvider(
*/
@NonNull String id,
@NonNull Class<?> beanClass,
-
//@ToString.Exclude @EqualsAndHashCode.Exclude
@NonNull Supplier<?> beanProvider) {
diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/ioc/SpringContextHolder.java b/commons/src/main/java/org/apache/causeway/commons/internal/ioc/SpringContextHolder.java
index edae73de62a..219441c97ce 100644
--- a/commons/src/main/java/org/apache/causeway/commons/internal/ioc/SpringContextHolder.java
+++ b/commons/src/main/java/org/apache/causeway/commons/internal/ioc/SpringContextHolder.java
@@ -26,6 +26,9 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Primary;
@@ -37,9 +40,6 @@
import org.apache.causeway.commons.internal.collections._Sets;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import org.jspecify.annotations.NonNull;
-import org.jspecify.annotations.Nullable;
-
/**
* <h1>- internal use only -</h1>
* <p>
@@ -51,8 +51,7 @@
* @since 2.0
*/
public record SpringContextHolder(
- @NonNull ApplicationContext springContext
- ) {
+ @NonNull ApplicationContext springContext) {
public Stream<SingletonBeanProvider> streamAllBeans() {
return Stream.of(springContext.getBeanDefinitionNames())
@@ -105,7 +104,6 @@ public <T> Optional<T> get(final @NonNull Class<T> requiredType) {
* @see #select(Class, Annotation[])
* @see #getSingletonElseFail(Class)
*/
- @SuppressWarnings("javadoc")
public <T> Can<T> select(final @NonNull Class<T> requiredType) {
var allMatchingBeans = springContext.getBeanProvider(requiredType)
.orderedStream()
@@ -128,7 +126,6 @@ public <T> Can<T> select(final @NonNull Class<T> requiredType) {
*
* @see #select(Class)
*/
- @SuppressWarnings("javadoc")
public <T> Can<T> select(
final @NonNull Class<T> requiredType,
final @Nullable Annotation[] qualifiers) {
@@ -165,12 +162,10 @@ public <T> Can<T> select(
* @return IoC managed singleton
* @throws NoSuchElementException - if the singleton is not resolvable
*/
- @SuppressWarnings("javadoc")
public <T> T getSingletonElseFail(final @NonNull Class<T> type) {
var candidates = select(type);
- if (candidates.getCardinality() == Cardinality.ZERO) {
+ if (candidates.getCardinality() == Cardinality.ZERO)
throw _Exceptions.noSuchElement("Cannot resolve singleton '%s'", type);
- }
return candidates.getFirstElseFail();
}
@@ -183,9 +178,8 @@ public <T> T getSingletonElseFail(final @NonNull Class<T> type) {
* @return non-null
*/
private static Set<Annotation> filterQualifiers(final @Nullable Annotation[] annotations) {
- if(_NullSafe.isEmpty(annotations)) {
+ if(_NullSafe.isEmpty(annotations))
return Collections.emptySet();
- }
return _NullSafe.stream(annotations)
.filter(SpringContextHolder::isGenericQualifier)
.collect(Collectors.toSet());
@@ -196,15 +190,12 @@ private static Set<Annotation> filterQualifiers(final @Nullable Annotation[] ann
* @return whether or not the annotation is a valid qualifier for Spring
*/
private static boolean isGenericQualifier(final Annotation annotation) {
- if(annotation==null) {
+ if(annotation==null)
return false;
- }
- if(annotation.annotationType().getAnnotationsByType(Qualifier.class).length>0) {
+ if(annotation.annotationType().getAnnotationsByType(Qualifier.class).length>0)
return true;
- }
- if(annotation.annotationType().equals(Primary.class)) {
+ if(annotation.annotationType().equals(Primary.class))
return true;
- }
return false;
}
diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/reflection/_ClassCache.java b/commons/src/main/java/org/apache/causeway/commons/internal/reflection/_ClassCache.java
index e56564da9c6..979a00303be 100644
--- a/commons/src/main/java/org/apache/causeway/commons/internal/reflection/_ClassCache.java
+++ b/commons/src/main/java/org/apache/causeway/commons/internal/reflection/_ClassCache.java
@@ -41,6 +41,7 @@
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
@@ -87,6 +88,10 @@ public enum Attribute {
* Corresponds to the bean name of Spring managed beans.
*/
SPRING_NAMED,
+ /**
+ * Corresponds to the bean qualifier of Spring managed beans.
+ */
+ SPRING_QUALIFIED,
/**
* Corresponds to DomainObject#mixinMethod().
*/
@@ -121,12 +126,12 @@ public boolean isNamed(final Class<?> type) {
public void setSpringNamed(final Class<?> beanClass, final String beanDefinitionName) {
_Assert.assertNotEmpty(beanDefinitionName);
- head(beanClass).attributeMap().put(Attribute.SPRING_NAMED, beanDefinitionName);
+ head(beanClass).putAttribute(Attribute.SPRING_NAMED, beanDefinitionName);
}
public String getLogicalName(final Class<?> type) {
var head = head(type);
- return Optional.ofNullable(head.attributeMap().get(Attribute.SPRING_NAMED))
+ return head.getAttribute(Attribute.SPRING_NAMED)
.or(()->Optional.ofNullable(head.named()))
.or(()->Optional.ofNullable(type.getCanonicalName()))
.orElseGet(type::getName);
@@ -270,7 +275,10 @@ public record ClassModelHead(
MergedAnnotations mergedAnnotations,
/** explicit name if any */
@Nullable String named,
- Map<Attribute, String> attributeMap) {
+ /**
+ * implemented as cache (supports computeIfAbsent)
+ */
+ Map<Attribute, Optional<String>> attributeMap) {
static ClassModelHead create(final Class<?> type) {
var mergedAnnotations = MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY);
@@ -300,9 +308,35 @@ public boolean hasJaxbRootElementSemantics() {
public Can<String> springProfiles() {
var profileAnnot = mergedAnnotations.get(Profile.class);
- if(!profileAnnot.isPresent()) return Can.empty();
- return Can.ofArray(profileAnnot.getStringArray("value"));
+ return profileAnnot.isPresent()
+ ? Can.ofArray(profileAnnot.getStringArray("value"))
+ : Can.empty();
}
+
+ /**
+ * Optionally the type's qualifier as specified via {@link Qualifier} annotation.
+ */
+ public Optional<String> qualifier() {
+ return attributeMap.computeIfAbsent(Attribute.SPRING_QUALIFIED, it->{
+ var qualifierAnnot = mergedAnnotations.get(Qualifier.class);
+ return qualifierAnnot.isPresent()
+ ? Optional.ofNullable(qualifierAnnot.getString("value"))
+ : Optional.empty();
+ });
+ }
+
+ public Optional<String> getAttribute(final Attribute attribute) {
+ var opt = attributeMap.get(attribute);
+ return opt!=null
+ ? opt
+ : Optional.empty();
+ }
+
+ public ClassModelHead putAttribute(final Attribute attribute, @Nullable final String value) {
+ attributeMap().put(attribute, Optional.ofNullable(value));
+ return this;
+ }
+
}
private record ClassModelBody(
diff --git a/core/config/src/main/java/org/apache/causeway/core/config/beans/CausewayBeanTypeClassifier.java b/core/config/src/main/java/org/apache/causeway/core/config/beans/CausewayBeanTypeClassifier.java
index 03b8de75134..8535c433303 100644
--- a/core/config/src/main/java/org/apache/causeway/core/config/beans/CausewayBeanTypeClassifier.java
+++ b/core/config/src/main/java/org/apache/causeway/core/config/beans/CausewayBeanTypeClassifier.java
@@ -84,38 +84,32 @@ public CausewayBeanMetaData classify(final @NonNull LogicalType logicalType, fin
: logicalType; // name is already inferred, when discovered by Causeway
if(ClassUtils.isPrimitiveOrWrapper(type)
- || type.isEnum()) {
+ || type.isEnum())
return CausewayBeanMetaData.value(named.get(), discoveredBy);
- }
- if(CollectionSemantics.valueOf(type).isPresent()) {
+ if(CollectionSemantics.valueOf(type).isPresent())
return CausewayBeanMetaData.collection(named.get(), discoveredBy);
- }
if(type.isInterface()
// modifier predicate must be called after testing for non-scalar type above,
// otherwise we'd get false positives
- || Modifier.isAbstract(type.getModifiers())) {
-
+ || Modifier.isAbstract(type.getModifiers()))
// apiNote: abstract types and interfaces cannot be vetoed
// and should also never be identified as ENTITY, VIEWMODEL or MIXIN
// however, concrete types that inherit abstract ones with vetoes,
// will effectively be vetoed through means of annotation synthesis
return CausewayBeanMetaData.interfaceOrAbstract(named.get(), discoveredBy);
- }
var typeHead = classCache().head(type);
// handle vetoing ...
- if(TypeVetoMarker.anyMatchOn(typeHead)) {
+ if(TypeVetoMarker.anyMatchOn(typeHead))
return CausewayBeanMetaData.vetoed(named.get(), discoveredBy); // reject
- }
var profiles = typeHead.springProfiles();
if(profiles.isNotEmpty()
- && !profiles.stream().anyMatch(this::isProfileActive)) {
+ && !profiles.stream().anyMatch(this::isProfileActive))
return CausewayBeanMetaData.vetoed(named.get(), discoveredBy); // reject
- }
// handle introspection veto (programmatic bean) ...
if(TypeProgrammaticMarker.anyMatchOn(typeHead)) {
@@ -127,51 +121,40 @@ public CausewayBeanMetaData classify(final @NonNull LogicalType logicalType, fin
}
// when implements ViewModel, yield VIEW_MODEL unless vetoed
- if(org.apache.causeway.applib.ViewModel.class.isAssignableFrom(type)) {
+ if(org.apache.causeway.applib.ViewModel.class.isAssignableFrom(type))
return CausewayBeanMetaData.viewModel(named.get(), discoveredBy);
- }
// value types
- if(typeHead.hasAnnotation(org.apache.causeway.applib.annotation.Value.class)) {
+ if(typeHead.hasAnnotation(org.apache.causeway.applib.annotation.Value.class))
return CausewayBeanMetaData.value(named.get(), discoveredBy);
- }
// domain service
- if(typeHead.hasAnnotation(DomainService.class)) {
+ if(typeHead.hasAnnotation(DomainService.class))
return CausewayBeanMetaData.springContributing(logicalType);
- }
// entity support
- if(typeHead.hasAnnotation(Entity.class)) {
+ if(typeHead.hasAnnotation(Entity.class))
return CausewayBeanMetaData.entity(named.get(), discoveredBy, PersistenceStack.JPA);
- }
// domain object
var aDomainObject = typeHead.annotation(DomainObject.class).orElse(null);
- if(aDomainObject!=null) {
- switch (aDomainObject.nature()) {
- case BEAN:
- return CausewayBeanMetaData.unspecified(named.get(), discoveredBy, BeanSort.MANAGED_BEAN_CONTRIBUTING);
- case MIXIN:
+ if(aDomainObject!=null)
+ return switch (aDomainObject.nature()) {
+ case BEAN -> CausewayBeanMetaData.unspecified(named.get(), discoveredBy, BeanSort.MANAGED_BEAN_CONTRIBUTING);
+ case MIXIN -> {
// memoize mixin main name
- typeHead.attributeMap().put(_ClassCache.Attribute.MIXIN_MAIN_METHOD_NAME, aDomainObject.mixinMethod());
- return CausewayBeanMetaData.mixin(named.get(), discoveredBy);
- case ENTITY:
- return CausewayBeanMetaData.entity(named.get(), discoveredBy, PersistenceStack.UNSPECIFIED);
- case VIEW_MODEL:
- case NOT_SPECIFIED:
- //because object is not associated with a persistence context unless discovered above
- return CausewayBeanMetaData.viewModel(named.get(), discoveredBy);
+ typeHead.putAttribute(_ClassCache.Attribute.MIXIN_MAIN_METHOD_NAME, aDomainObject.mixinMethod());
+ yield CausewayBeanMetaData.mixin(named.get(), discoveredBy);
}
- }
+ case ENTITY -> CausewayBeanMetaData.entity(named.get(), discoveredBy, PersistenceStack.UNSPECIFIED);
+ case VIEW_MODEL, NOT_SPECIFIED -> /*because object is not associated with a persistence context unless discovered above */ CausewayBeanMetaData.viewModel(named.get(), discoveredBy);
+ };
- if(typeHead.hasJaxbRootElementSemantics()) {
+ if(typeHead.hasJaxbRootElementSemantics())
return CausewayBeanMetaData.viewModel(named.get(), discoveredBy);
- }
- if(typeHead.hasAnnotation(Component.class)) {
+ if(typeHead.hasAnnotation(Component.class))
return CausewayBeanMetaData.unspecified(logicalType, discoveredBy, BeanSort.MANAGED_BEAN_NOT_CONTRIBUTING);
- }
// unless explicitly declared otherwise, map records to unknown
if(type.isRecord()) {
@@ -182,9 +165,8 @@ public CausewayBeanMetaData classify(final @NonNull LogicalType logicalType, fin
: CausewayBeanMetaData.unspecified(named.get(), discoveredBy, BeanSort.UNKNOWN);
}
- if(Serializable.class.isAssignableFrom(type)) {
+ if(Serializable.class.isAssignableFrom(type))
return CausewayBeanMetaData.unspecified(named.get(), discoveredBy, BeanSort.VALUE);
- }
return CausewayBeanMetaData.unspecified(named.get(), discoveredBy, BeanSort.UNKNOWN);
}
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 625d6271140..5da343ad2c7 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
@@ -108,7 +108,8 @@ private Predicate<ResolvedMethod> isMixinMainMethod(final @NonNull ProcessClassC
}
// lookup attribute from class-cache as it should have been already processed by the BeanTypeClassifier
var cls = processClassContext.getCls();
- var mixinMainMethodName = getClassCache().head(cls).attributeMap().get(Attribute.MIXIN_MAIN_METHOD_NAME);
+ var mixinMainMethodName = getClassCache().head(cls).getAttribute(Attribute.MIXIN_MAIN_METHOD_NAME)
+ .orElse(null);
return method->method.name().equals(mixinMainMethodName);
}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/value/ValueFacetAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/value/ValueFacetAbstract.java
index dac63e3298d..5276b6d452a 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/value/ValueFacetAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/value/ValueFacetAbstract.java
@@ -23,7 +23,7 @@
import java.util.function.Predicate;
import java.util.stream.Stream;
-import org.springframework.beans.factory.annotation.Qualifier;
+import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.apache.causeway.applib.Identifier;
@@ -38,13 +38,11 @@
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._Strings;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import org.apache.causeway.commons.internal.reflection._Annotations;
+import org.apache.causeway.commons.internal.reflection._ClassCache;
import org.apache.causeway.core.metamodel.facetapi.Facet;
import org.apache.causeway.core.metamodel.facetapi.FacetAbstract;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
-import org.apache.causeway.core.metamodel.facets.objectvalue.valuesemantics.ValueSemanticsSelectingFacet;
import org.apache.causeway.core.metamodel.interactions.managed.ManagedProperty;
import org.apache.causeway.core.metamodel.interactions.managed.ParameterNegotiationModel;
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
@@ -52,8 +50,9 @@
import org.apache.causeway.core.metamodel.spec.feature.MixedInAction;
import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
+import org.apache.causeway.core.metamodel.util.Facets;
+
import lombok.Getter;
-import org.jspecify.annotations.NonNull;
import lombok.RequiredArgsConstructor;
public abstract class ValueFacetAbstract<T>
@@ -119,9 +118,8 @@ public final LogicalType getLogicalType() {
@Override
public ValueSemanticsProvider.Context createValueSemanticsContext(final @Nullable ObjectFeature feature) {
var iaProvider = super.getInteractionService();
- if(iaProvider==null) {
+ if(iaProvider==null)
return null; // JUnit context
- }
return new ValueSemanticsProvider.Context(
feature!=null
? feature.getFeatureIdentifier()
@@ -147,7 +145,7 @@ public final String enstring(final Format format, final T value) {
public Optional<OrderRelation<T, ?>> selectDefaultOrderRelation() {
return getAllValueSemantics()
.stream()
- .filter(isMatchingAnyOf(Can.empty()))
+ .filter(isMatchingAnyOf(Optional.empty()))
.map(ValueSemanticsProvider::getOrderRelation)
.filter(_NullSafe::isPresent)
.findFirst()
@@ -160,7 +158,7 @@ public final String enstring(final Format format, final T value) {
public Optional<ValueSemanticsProvider<T>> selectDefaultSemantics() {
return getAllValueSemantics()
.stream()
- .filter(isMatchingAnyOf(Can.empty()))
+ .filter(isMatchingAnyOf(Optional.empty()))
.filter(_NullSafe::isPresent)
.findFirst();
}
@@ -171,7 +169,7 @@ public Optional<ValueSemanticsProvider<T>> selectDefaultSemantics() {
public Optional<DefaultsProvider<T>> selectDefaultDefaultsProvider() {
return getAllValueSemantics()
.stream()
- .filter(isMatchingAnyOf(Can.empty()))
+ .filter(isMatchingAnyOf(Optional.empty()))
.map(ValueSemanticsProvider::getDefaultsProvider)
.filter(_NullSafe::isPresent)
.findFirst();
@@ -191,7 +189,7 @@ public Optional<DefaultsProvider<T>> selectDefaultsProviderForAttribute(@Nullabl
public Optional<Parser<T>> selectDefaultParser() {
return getAllValueSemantics()
.stream()
- .filter(isMatchingAnyOf(Can.empty()))
+ .filter(isMatchingAnyOf(Optional.empty()))
.map(ValueSemanticsProvider::getParser)
.filter(_NullSafe::isPresent)
.findFirst();
@@ -216,7 +214,7 @@ public Parser<T> fallbackParser(final Identifier featureIdentifier) {
public Optional<Renderer<T>> selectDefaultRenderer() {
return getAllValueSemantics()
.stream()
- .filter(isMatchingAnyOf(Can.empty()))
+ .filter(isMatchingAnyOf(Optional.empty()))
.map(ValueSemanticsProvider::getRenderer)
.filter(_NullSafe::isPresent)
.findFirst();
@@ -243,7 +241,7 @@ public Optional<TemporalSupport<T>> selectTemporalSupportForFeature(final @Nulla
? streamValueSemanticsHonoringQualifiers(feature)
: getAllValueSemantics().stream())
.filter(TemporalSupport.class::isInstance)
- .map(temporalDecomposer->_Casts.<TemporalSupport<T>>uncheckedCast(temporalDecomposer))
+ .<TemporalSupport<T>>map(_Casts::uncheckedCast)
.findFirst();
}
@@ -275,7 +273,7 @@ public Optional<ObjectAction> selectCompositeValueMixinForProperty(final Managed
public static <X> Parser<X> fallbackParser(
final LogicalType valueType,
final Identifier featureIdentifier) {
- return new PseudoParserWithMessage<X>(String
+ return new PseudoParserWithMessage<>(String
.format("Could not find a parser for type %s "
+ "in the context of %s",
valueType,
@@ -285,7 +283,7 @@ public static <X> Parser<X> fallbackParser(
public static <X> Renderer<X> fallbackRenderer(
final LogicalType valueType,
final Identifier featureIdentifier) {
- return new PseudoRendererWithMessage<X>(String
+ return new PseudoRendererWithMessage<>(String
.format("Could not find a renderer for type %s "
+ "in the context of %s",
valueType,
@@ -298,46 +296,37 @@ private Stream<ValueSemanticsProvider<T>> streamValueSemanticsHonoringQualifiers
final @NonNull ObjectFeature feature) {
return getAllValueSemantics()
.stream()
- .filter(isMatchingAnyOf(qualifiersAccepted(feature)));
+ .filter(isMatchingAnyOf(Facets.valueQualifier(feature)));
}
- private Can<String> qualifiersAccepted(final @NonNull ObjectFeature feature) {
- return feature.lookupFacet(ValueSemanticsSelectingFacet.class)
- .map(ValueSemanticsSelectingFacet::value)
- .map(_Strings::emptyToNull)
- .stream()
- .collect(Can.toCan());
+ private Optional<String> qualifierProvided(final @NonNull ValueSemanticsProvider<T> semProv) {
+ return _ClassCache.getInstance().head(semProv.getClass())
+ .qualifier();
}
- private Predicate<ValueSemanticsProvider<T>> isMatchingAnyOf(final Can<String> qualifiersAccepted) {
- return valueSemantics->{
+ private Predicate<ValueSemanticsProvider<T>> isMatchingAnyOf(final Optional<String> qualifierRequired) {
+ return semProv -> isMatchingAnyOf(qualifierRequired, qualifierProvided(semProv));
+ }
- // qualifiers accepted vs. qualifiers present on bean type
- // 1. empty vs. empty -> accept
- // 2. empty vs. not-empty -> reject
- // 3. not-empty vs. empty -> reject
- // 4. not-empty vs. not-empty -> accept when any match
+ private boolean isMatchingAnyOf(
+ final Optional<String> qualifierRequired,
+ final Optional<String> qualifierProvided) {
- var qualifiersOnBean =
- _Annotations
- .synthesize(valueSemantics.getClass(), Qualifier.class) //TODO memoize somewhere
- .map(Qualifier::value)
- .stream()
- .map(_Strings::emptyToNull)
- .collect(Can.toCan());
+ // qualifiers accepted vs. qualifiers present on bean type
+ // 1. empty vs. empty -> accept
+ // 2. empty vs. not-empty -> reject
+ // 3. not-empty vs. empty -> reject
+ // 4. not-empty vs. not-empty -> accept when any match
- if(qualifiersAccepted.isEmpty()
- && qualifiersOnBean.isEmpty()) {
- return true;
- }
+ if(qualifierRequired.isEmpty()
+ && qualifierProvided.isEmpty())
+ return true;
- if(qualifiersAccepted.isNotEmpty()
- && qualifiersOnBean.isNotEmpty()) {
- return qualifiersAccepted.stream().anyMatch(qualifiersOnBean::contains);
- }
+ if(qualifierRequired.isPresent()
+ && qualifierProvided.isPresent())
+ return qualifierRequired.get().equals(qualifierProvided.get());
- return false;
- };
+ return false;
}
/**
@@ -348,8 +337,9 @@ private Predicate<ValueSemanticsProvider<T>> isMatchingAnyOf(final Can<String> q
* @param feature - optionally provides (custom) qualifier
*/
protected Optional<ObjectAction> resolveCompositeValueMixinForFeature(final ObjectFeature feature) {
- return qualifiersAccepted(feature).add("default")
- .stream()
+ return Stream.concat(
+ Facets.valueQualifier(feature).stream(),
+ Stream.of("default"))
.map(qualifier->feature.getElementType().getAction(qualifier, MixedIn.ONLY))
.filter(Optional::isPresent)
.map(Optional::get)
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/value/annotcfg/ValueFacetForValueAnnotationOrAnyMatchingValueSemanticsFacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/value/annotcfg/ValueFacetForValueAnnotationOrAnyMatchingValueSemanticsFacetFactory.java
index 232e0e984d1..849fa88d1a0 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/value/annotcfg/ValueFacetForValueAnnotationOrAnyMatchingValueSemanticsFacetFactory.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/value/annotcfg/ValueFacetForValueAnnotationOrAnyMatchingValueSemanticsFacetFactory.java
@@ -99,12 +99,13 @@ public void process(final ProcessClassContext processClassContext) {
// -- HELPER
private <T> Optional<ValueFacet<T>> addAllFacetsForValueSemantics(
- final Identifier identifier,
+ final Identifier featureId,
final Class<T> valueClass,
final ObjectSpecification valueSpec,
final Optional<Value> valueIfAny) {
- var semanticsProviders = getValueSemanticsResolver().selectValueSemantics(identifier, valueClass);
+ var semanticsProviders = getValueSemanticsResolver().streamValueSemantics(valueClass)
+ .collect(Can.toCan());
if(semanticsProviders.isEmpty()) {
if(valueIfAny.isPresent()) {
log.warn("could not find a ValueSemanticsProvider for value type {}; "
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/value/vsp/ValueFacetUsingSemanticsProvider.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/value/vsp/ValueFacetUsingSemanticsProvider.java
index aadfcb895d8..27380b46528 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/value/vsp/ValueFacetUsingSemanticsProvider.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/value/vsp/ValueFacetUsingSemanticsProvider.java
@@ -30,7 +30,7 @@ public static <T> ValueFacetUsingSemanticsProvider<T> create(
final Class<T> valueClass,
final Can<ValueSemanticsProvider<T>> valueSemantics,
final FacetHolder holder) {
- return new ValueFacetUsingSemanticsProvider<T>(valueClass, valueSemantics, holder);
+ return new ValueFacetUsingSemanticsProvider<>(valueClass, valueSemantics, holder);
}
protected ValueFacetUsingSemanticsProvider(
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/registry/ServiceRegistryDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/registry/ServiceRegistryDefault.java
index 1c63f466b6f..52bce4ed6bd 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/registry/ServiceRegistryDefault.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/registry/ServiceRegistryDefault.java
@@ -28,16 +28,22 @@
import jakarta.inject.Inject;
import jakarta.inject.Named;
+import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Service;
+import org.springframework.util.ClassUtils;
import org.apache.causeway.applib.annotation.PriorityPrecedence;
import org.apache.causeway.applib.id.LogicalType;
import org.apache.causeway.applib.services.registry.ServiceRegistry;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.internal.base._Lazy;
+import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.internal.collections._Maps;
import org.apache.causeway.commons.internal.ioc.SingletonBeanProvider;
+import org.apache.causeway.commons.internal.reflection._ClassCache;
import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry;
import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
@@ -53,9 +59,17 @@
@Qualifier("Default")
public final class ServiceRegistryDefault implements ServiceRegistry {
+ private final CausewaySystemEnvironment causewaySystemEnvironment;
+ private final CausewayBeanTypeRegistry causewayBeanTypeRegistry;
+
// enforces provisioning order (this is a depends-on relationship)
- @Inject private CausewaySystemEnvironment causewaySystemEnvironment;
- @Inject private CausewayBeanTypeRegistry causewayBeanTypeRegistry;
+ @Inject
+ public ServiceRegistryDefault(
+ final CausewaySystemEnvironment causewaySystemEnvironment,
+ final CausewayBeanTypeRegistry causewayBeanTypeRegistry) {
+ this.causewaySystemEnvironment = causewaySystemEnvironment;
+ this.causewayBeanTypeRegistry = causewayBeanTypeRegistry;
+ }
@Override
public Optional<SingletonBeanProvider> lookupRegisteredBeanById(final LogicalType id) {
@@ -98,6 +112,9 @@ private Map<String, SingletonBeanProvider> enumerateContributingDomainServices()
.forEach(singletonProvider->
managedBeanAdapterByName.put(singletonProvider.id(), singletonProvider));
+ //TODO perhaps find a better place where to do this
+ //primeQualifierLookup(causewaySystemEnvironment);
+
return managedBeanAdapterByName;
}
@@ -108,4 +125,32 @@ private Predicate<SingletonBeanProvider> contributes() {
: false;
}
+ /**
+ * Collects qualifier information into the _ClassCache.
+ */
+ private static void primeQualifierLookup(final CausewaySystemEnvironment causewaySystemEnvironment) {
+ var classCache = _ClassCache.getInstance();
+ var springContext = causewaySystemEnvironment.springContextHolder().springContext();
+ var beanFactory = ((ConfigurableApplicationContext) springContext).getBeanFactory();
+
+ Stream.of(springContext.getBeanDefinitionNames())
+ .forEach(name->{
+ var type = ClassUtils.getUserClass(springContext.getType(name));
+ lookupQualifier(name, beanFactory)
+ .ifPresent(qualifier->
+ classCache.head(type).putAttribute(_ClassCache.Attribute.SPRING_QUALIFIED, qualifier));
+ });
+ }
+
+ private static Optional<String> lookupQualifier(final String id, final ConfigurableListableBeanFactory beanFactory) {
+ var beanDefinition = beanFactory.getBean(id);
+ if (beanDefinition instanceof AnnotatedBeanDefinition annotatedBeanDefinition)
+ return Optional.ofNullable(
+ annotatedBeanDefinition.getMetadata().getAnnotationAttributes(Qualifier.class.getName()))
+ .map(it->it.get("value"))
+ .map(String.class::cast)
+ .map(_Strings::emptyToNull);
+ return Optional.empty();
+ }
+
}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/schema/SchemaValueMarshallerAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/schema/SchemaValueMarshallerAbstract.java
index 3b9f51fd5ac..15c569a2940 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/schema/SchemaValueMarshallerAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/schema/SchemaValueMarshallerAbstract.java
@@ -48,6 +48,7 @@
import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
+import org.apache.causeway.core.metamodel.util.Facets;
import org.apache.causeway.schema.cmd.v2.ActionDto;
import org.apache.causeway.schema.cmd.v2.ParamDto;
import org.apache.causeway.schema.cmd.v2.PropertyDto;
@@ -73,7 +74,7 @@ public record Context<T>(
public static <T> Context<T> forNonValue(
final Class<T> correspondingClass,
final ObjectFeature feature) {
- return new Context<T>(correspondingClass, feature,
+ return new Context<>(correspondingClass, feature,
feature.getFeatureType().isCollection()
? ValueType.COLLECTION
: ValueType.REFERENCE,
@@ -237,7 +238,7 @@ public final ManagedObject recoverParameterFrom(
}
// -- HELPER
-
+
/**
* Recovers an {@link Identifier} for given {@code logicalMemberIdentifier}.
*/
@@ -275,10 +276,11 @@ private static Identifier memberIdentifierFor(
private <T> Context<T> newContext(
final Class<T> valueCls,
- final ObjectFeature feature){
+ final ObjectFeature feature) {
+
return getValueSemanticsResolver()
- .selectValueSemantics(feature.getFeatureIdentifier(), valueCls)
- .getFirst()
+ .streamValueSemantics(valueCls, Facets.valueQualifier(feature).orElse(null))
+ .findFirst()
.map(valueSemantics->Context.forValue(valueCls, feature, valueSemantics))
.orElseGet(()->Context.forNonValue(valueCls, feature));
}
@@ -331,9 +333,8 @@ protected Can<ManagedObject> recoverCollectionOfValues(
_Assert.assertEquals(valueTypeHelper.schemaValueType(), collectionDto.getType());
- if(_NullSafe.isEmpty(collectionDto.getValue())) {
+ if(_NullSafe.isEmpty(collectionDto.getValue()))
return Can.empty();
- }
var elementDtos = collectionDto.getValue();
var list = new ArrayList<ManagedObject>(elementDtos.size());
@@ -353,9 +354,8 @@ protected Can<ManagedObject> recoverCollectionOfValues(
protected Can<ManagedObject> recoverCollectionOfReferences(
final CollectionDto collectionDto) {
- if(_NullSafe.isEmpty(collectionDto.getValue())) {
+ if(_NullSafe.isEmpty(collectionDto.getValue()))
return Can.empty();
- }
var elementDtos = collectionDto.getValue();
var list = new ArrayList<ManagedObject>(elementDtos.size());
@@ -375,11 +375,10 @@ private ManagedObject recoverValueOrReference(
if(valueWithTypeDto==null
|| (valueWithTypeDto.isNull()!=null
- && valueWithTypeDto.isNull())) {
+ && valueWithTypeDto.isNull()))
return cardinalityConstraint.isMultiple()
? ManagedObject.packed(feature.getElementType(), Can.empty())
: ManagedObject.empty(feature.getElementType());
- }
var valueCls = feature.getElementType().getCorrespondingClass();
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/Facets.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/Facets.java
index d79bc43d309..bf1b868eca1 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/Facets.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/Facets.java
@@ -23,6 +23,7 @@
import java.util.function.Predicate;
import java.util.stream.Stream;
+import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.util.ClassUtils;
@@ -40,6 +41,7 @@
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._Strings;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
import org.apache.causeway.commons.internal.factory._InstanceUtil;
import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedType;
@@ -74,6 +76,7 @@
import org.apache.causeway.core.metamodel.facets.objectvalue.maxlen.MaxLengthFacet;
import org.apache.causeway.core.metamodel.facets.objectvalue.multiline.MultiLineFacet;
import org.apache.causeway.core.metamodel.facets.objectvalue.typicallen.TypicalLengthFacet;
+import org.apache.causeway.core.metamodel.facets.objectvalue.valuesemantics.ValueSemanticsSelectingFacet;
import org.apache.causeway.core.metamodel.facets.param.parameter.precpol.PrecedingParametersPolicyFacet;
import org.apache.causeway.core.metamodel.interactions.managed.ManagedProperty;
import org.apache.causeway.core.metamodel.interactions.managed.ParameterNegotiationModel;
@@ -428,6 +431,15 @@ public <X> Optional<ValueSemanticsProvider<X>> valueDefaultSemantics(
.map(_Casts::uncheckedCast);
}
+ /**
+ * Optionally the qualifier string, that selects for a (qualified) {@link ValueSemanticsProvider}.
+ */
+ public Optional<String> valueQualifier(@NonNull final ObjectFeature feature) {
+ return feature.lookupFacet(ValueSemanticsSelectingFacet.class)
+ .map(ValueSemanticsSelectingFacet::value)
+ .map(_Strings::emptyToNull);
+ }
+
@SuppressWarnings("unchecked")
public <X> Optional<ValueSerializer<X>> valueSerializer(
final ObjectSpecification objectSpec,
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuetypes/ValueSemanticsResolverDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuetypes/ValueSemanticsResolverDefault.java
index f844c869438..d2257f12e09 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuetypes/ValueSemanticsResolverDefault.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuetypes/ValueSemanticsResolverDefault.java
@@ -25,22 +25,23 @@
import jakarta.inject.Inject;
import jakarta.inject.Named;
+import org.jspecify.annotations.Nullable;
+
import org.springframework.stereotype.Service;
import org.springframework.util.ClassUtils;
-import org.apache.causeway.applib.Identifier;
import org.apache.causeway.applib.annotation.PriorityPrecedence;
import org.apache.causeway.applib.services.i18n.TranslationService;
import org.apache.causeway.applib.value.semantics.ValueSemanticsProvider;
import org.apache.causeway.applib.value.semantics.ValueSemanticsResolver;
-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._Strings;
import org.apache.causeway.commons.internal.collections._Maps;
+import org.apache.causeway.commons.internal.reflection._ClassCache;
import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
import org.apache.causeway.core.metamodel.valuesemantics.EnumValueSemantics;
-import org.jspecify.annotations.NonNull;
import lombok.RequiredArgsConstructor;
@Service
@@ -67,18 +68,27 @@ public <T> Stream<ValueSemanticsProvider<T>> streamValueSemantics(final Class<T>
}
@Override
- public <T> Can<ValueSemanticsProvider<T>> selectValueSemantics(
- final @NonNull Identifier featureIdentifier,
- final Class<T> valueType) {
- //FIXME[CAUSEWAY-2877] honor customizations
+ public <T> Stream<ValueSemanticsProvider<T>> streamValueSemantics(
+ final Class<T> valueType,
+ @Nullable final String _requiredQualifier) {
+
+ var requiredQualifier = _Strings.emptyToNull(_requiredQualifier);
+ if(requiredQualifier==null)
+ return streamValueSemantics(valueType);
+
+ var classCache = _ClassCache.getInstance();
+
return streamValueSemantics(valueType)
- .collect(Can.toCan());
+ .filter(semProv->
+ classCache.head(ClassUtils.getUserClass(semProv)).qualifier()
+ .map(qualifierProvided->qualifierProvided.equals(requiredQualifier))
+ .orElse(false));
}
@Override
public Stream<Class<?>> streamClassesWithValueSemantics() {
return _NullSafe.stream(valueSemanticsProviders)
- .map(ValueSemanticsProvider::getCorrespondingClass);
+ .map(ValueSemanticsProvider::getCorrespondingClass);
}
// -- HELPER
diff --git a/viewers/wicket/ui-test/src/test/java/org/apache/causeway/viewer/wicket/ui/test/components/scalars/ConverterTester.java b/viewers/wicket/ui-test/src/test/java/org/apache/causeway/viewer/wicket/ui/test/components/scalars/ConverterTester.java
index ed1956beb5c..3088c500460 100644
--- a/viewers/wicket/ui-test/src/test/java/org/apache/causeway/viewer/wicket/ui/test/components/scalars/ConverterTester.java
+++ b/viewers/wicket/ui-test/src/test/java/org/apache/causeway/viewer/wicket/ui/test/components/scalars/ConverterTester.java
@@ -29,6 +29,7 @@
import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -36,9 +37,7 @@
import org.springframework.boot.test.util.TestPropertyValues;
-import org.apache.causeway.applib.Identifier;
import org.apache.causeway.applib.clock.VirtualClock;
-import org.apache.causeway.applib.id.LogicalType;
import org.apache.causeway.applib.locale.UserLocale;
import org.apache.causeway.applib.services.clock.ClockService;
import org.apache.causeway.applib.services.iactn.InteractionContext;
@@ -103,10 +102,9 @@ public ConverterTester(
mmc.getServiceInjector().injectServicesInto(valueSemantics);
// pre-requisites for testing
- var identifier = Identifier.classIdentifier(LogicalType.fqcn(valueType));
var reg = mmc.getServiceRegistry().lookupServiceElseFail(ValueSemanticsResolver.class);
- assertNotNull(reg.selectValueSemantics(identifier, valueType));
- assertTrue(reg.selectValueSemantics(identifier, valueType).isNotEmpty());
+ var sem = reg.streamValueSemantics(valueType).toList();
+ assertFalse(sem.isEmpty());
assertNotNull(mmc.getServiceRegistry().lookupServiceElseFail(InteractionService.class));
assertNotNull(mmc.getInteractionService());
}
@@ -125,7 +123,7 @@ public ConverterBasedOnValueSemantics<T> converterForProperty(
var customerSpec = mmc.getSpecificationLoader().specForTypeElseFail(type);
var prop = customerSpec.getPropertyElseFail("value");
var propType = (Class<T>) prop.getElementType().getCorrespondingClass();
- return new ConverterBasedOnValueSemantics<T>(propType, prop, representation);
+ return new ConverterBasedOnValueSemantics<>(propType, prop, representation);
}
public void runWithInteraction(final @NonNull ThrowingRunnable runnable) {