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

ahuber pushed a commit to branch 3971-layout.switching
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/3971-layout.switching by this 
push:
     new 107c509ca0b CAUSEWAY-3971: implements FacetRank (failing tests)
107c509ca0b is described below

commit 107c509ca0b1c72f6cceaa9e9b4d31fa3af6c659
Author: andi-huber <[email protected]>
AuthorDate: Tue Mar 3 09:44:25 2026 +0100

    CAUSEWAY-3971: implements FacetRank (failing tests)
    
    captures the various lanes per facet rank
    hello world reproducer is looking good now
---
 .../core/metamodel/facetapi/FacetHolderSimple.java |   7 +-
 .../core/metamodel/facetapi/FacetRank.java         | 114 +++++++++++
 .../core/metamodel/facetapi/FacetRanking.java      | 220 ++++++++++++---------
 .../core/metamodel/facetapi/FacetUtil.java         |  59 +++---
 .../core/metamodel/facetapi/QualifiedFacet.java    |  35 ++++
 ...ObjectLayoutAnnotationUsingCssClassUiEvent.java |   4 +-
 ...mainObjectLayoutAnnotationUsingIconUiEvent.java |   4 +-
 ...ainObjectLayoutAnnotationUsingTitleUiEvent.java |   5 +-
 .../object/layout/LayoutPrefixFacetForUiEvent.java |   2 +-
 ...udeAnnotationEnforcesMetamodelContribution.java |   5 +-
 .../metamodel/services/grid/GridLoadingTest.java   |   2 +-
 .../menubars/bootstrap/MenuBarsServiceBSTest.java  |   5 +-
 12 files changed, 315 insertions(+), 147 deletions(-)

diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetHolderSimple.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetHolderSimple.java
index 09cf4f42ea0..ba4f5bb4492 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetHolderSimple.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetHolderSimple.java
@@ -27,7 +27,6 @@
 
 import org.apache.causeway.applib.Identifier;
 import org.apache.causeway.core.metamodel.context.MetaModelContext;
-import org.apache.causeway.core.metamodel.util.Facets;
 
 /**
  * Provides a (simple) list of {@link Facet}s.
@@ -87,11 +86,10 @@ public void addFacet(final @NonNull Facet facet) {
 
     @Override
     public <T extends Facet> T getFacet(final Class<T> facetType) {
-        var qualifier = Facets.qualifier(this);
         synchronized(rankingByType) {
 
             return getFacetRanking(facetType)
-                .flatMap(facetRanking->facetRanking.getWinner(facetType, 
qualifier))
+                .flatMap(facetRanking->facetRanking.getWinner(facetType))
                 .orElse(null);
 
             //return uncheckedCast(snapshot.get().get(facetType));
@@ -100,12 +98,11 @@ public <T extends Facet> T getFacet(final Class<T> 
facetType) {
 
     @Override
     public Stream<Facet> streamFacets() {
-        var qualifier = Facets.qualifier(this);
         synchronized(rankingByType) {
             // consumers should play nice and don't take too long (as we have 
a lock)
             //return snapshot.get().values().stream();
             return streamFacetRankings()
-                
.flatMap(facetRanking->facetRanking.getWinner(facetRanking.facetType(), 
qualifier)
+                
.flatMap(facetRanking->facetRanking.getWinner(facetRanking.facetType())
                         .stream());
         }
     }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetRank.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetRank.java
new file mode 100644
index 00000000000..5d58c877d72
--- /dev/null
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetRank.java
@@ -0,0 +1,114 @@
+/*
+ *  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.facetapi;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.jspecify.annotations.Nullable;
+
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.internal.assertions._Assert;
+import org.apache.causeway.commons.internal.collections._Lists;
+import org.apache.causeway.commons.internal.collections._Multimaps;
+import org.apache.causeway.core.metamodel.facetapi.QualifiedFacet.Key;
+
+/**
+ * Multiple {@link FacetRank}(s) are collected into a single {@link 
FacetRanking}.
+ *
+ * @apiNote not thread-safe
+ */
+record FacetRank(
+        Class<? extends Facet> facetType,
+        Facet.Precedence precedence,
+        _Multimaps.ListMultimap<QualifiedFacet.Key, Facet> facetsByQualifier) {
+
+    FacetRank(
+            final Class<? extends Facet> facetType,
+            final Facet.Precedence precedence) {
+        this(facetType, precedence, _Multimaps.newListMultimap());
+        //this(facetType, precedence, 
_Multimaps.newListMultimap(ConcurrentSkipListMap::new, 
CopyOnWriteArrayList::new));
+    }
+
+    QualifiedFacet.Key key(final @Nullable String qualifier) {
+        return new QualifiedFacet.Key(facetType, qualifier);
+    }
+
+    FacetRank add(final @Nullable Facet facet) {
+        if(facet==null)
+            return this; // no-op
+
+        _Assert.assertEquals(this.precedence(), facet.precedence());
+        _Assert.assertEquals(this.facetType(), facet.facetType());
+
+        facetsByQualifier.putElement(QualifiedFacet.Key.forFacet(facet), 
facet);
+        return this;
+    }
+
+    /**
+     * Whether this rank contains at least one matching {@link QualifiedFacet}.
+     */
+    boolean matches(final String qualifier) {
+        return facetsByQualifier.containsKey(key(qualifier));
+    }
+
+    Can<Facet> facetsMatching(final Key key) {
+        return lookupQualified(key)
+            .filter(list->!list.isEmpty())
+            .or(()->lookupUnqualified(key)
+                    .filter(list->!list.isEmpty()))
+            .map(Can::ofCollection)
+            .orElseGet(Can::empty);
+    }
+
+    /**
+     * Rules in order of strength:
+     * <ul>
+     * <li>all matching {@link QualifiedFacet}(s) take precedence</li>
+     * <li>all non-matching {@link QualifiedFacet}(s) must be ignored</li>
+     * <li>later take precedence over earlier</li>
+     * </ul>
+     */
+    Optional<Facet> findBest(final QualifiedFacet.Key key) {
+        return lookupQualified(key)
+            .flatMap(_Lists::lastElement)
+            .or(()->lookupUnqualified(key)
+                    .flatMap(_Lists::lastElement));
+    }
+
+    boolean hasBest(final QualifiedFacet.Key key) {
+        return isNotEmpty(lookupQualified(key))
+            || isNotEmpty(lookupUnqualified(key));
+    }
+
+    // -- HELPER
+
+    private Optional<List<Facet>> lookupQualified(final QualifiedFacet.Key 
key) {
+        return Optional.ofNullable(facetsByQualifier.get(key.toQualified()));
+    }
+    private Optional<List<Facet>> lookupUnqualified(final QualifiedFacet.Key 
key) {
+        return Optional.ofNullable(facetsByQualifier.get(key.toUnqualified()));
+    }
+    private static <T> boolean isNotEmpty(final Optional<List<T>> listOpt) {
+        return listOpt
+            .map(list->!list.isEmpty())
+            .orElse(false);
+    }
+
+}
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetRanking.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetRanking.java
index 823b8999531..17a4353f13e 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetRanking.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetRanking.java
@@ -18,12 +18,14 @@
  */
 package org.apache.causeway.core.metamodel.facetapi;
 
-import java.util.Comparator;
+import java.util.Map;
+import java.util.NavigableMap;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentSkipListMap;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiConsumer;
-import java.util.function.Predicate;
 
 import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
@@ -31,11 +33,9 @@
 import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.internal.assertions._Assert;
 import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.base._Reduction;
-import org.apache.causeway.commons.internal.collections._Lists;
-import org.apache.causeway.commons.internal.collections._Multimaps;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.core.metamodel.facetapi.Facet.Precedence;
+import org.apache.causeway.core.metamodel.util.Facets;
 
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
@@ -62,11 +62,10 @@ public final class FacetRanking {
 
     @Getter @Accessors(fluent = true) private final @NonNull Class<? extends 
Facet> facetType;
 
-    private final _Multimaps.@NonNull ListMultimap<Facet.Precedence, Facet> 
facetsByPrecedence
-            = _Multimaps.newSortedConcurrentListMultimap();
+    private final NavigableMap<Facet.Precedence, FacetRank> ranksByPrecedence 
= new ConcurrentSkipListMap<>();
 
     private final @NonNull AtomicReference<Facet> eventFacetRef = new 
AtomicReference<>();
-    private final @NonNull AtomicReference<Precedence> topPrecedenceRef = new 
AtomicReference<>();
+    private final @NonNull Map<QualifiedFacet.Key, Precedence> 
topPrecedenceRef = new ConcurrentHashMap<>();
 
     /**
      * @return whether the top rank changed,
@@ -79,6 +78,7 @@ public boolean add(final @NonNull Facet facet) {
         // guard against invalidly mocked facets
         var facetPrecedence = Objects.requireNonNull(facet.precedence(),
                 ()->String.format("facet %s declares no precedence", 
facet.getClass()));
+        var key = QualifiedFacet.Key.forFacet(facet);
 
         // handle top priority (EVENT) facets separately
         if(facetPrecedence.isEvent()) {
@@ -91,7 +91,8 @@ public boolean add(final @NonNull Facet facet) {
                                 facet.getClass());
                 return facet;
             });
-            topPrecedenceRef.set(facetPrecedence);
+
+            topPrecedenceRef.put(key, facetPrecedence);
             return true; // changes apply
         }
 
@@ -100,12 +101,12 @@ public boolean add(final @NonNull Facet facet) {
                 .orElse(-1);
 
         if(facetPrecedence.ordinal() > currentTopOrdinal) {
-            topPrecedenceRef.set(facetPrecedence);
+            topPrecedenceRef.put(key, facetPrecedence);
         }
 
-        var currentTopRankOrdinal = facetsByPrecedence.isEmpty()
+        var currentTopRankOrdinal = ranksByPrecedence.isEmpty() //FIXME needs 
context
                 ? -1
-                : 
facetsByPrecedence.asNavigableMapElseFail().lastKey().ordinal();
+                : ranksByPrecedence.lastKey().ordinal();
 
         var changesTopRank = facetPrecedence.ordinal() >= 
currentTopRankOrdinal;
 
@@ -114,16 +115,18 @@ public boolean add(final @NonNull Facet facet) {
         // However, there are use-cases, where access to all facets of a given 
type are required,
         // regardless of facet-precedence (eg. MemberNamedFacets).
         if(changesTopRank
-            || facet.isPopulateAllFacetRanks()) {
-            facetsByPrecedence.putElement(facetPrecedence, facet);
+                || facet.isPopulateAllFacetRanks()) {
+            var rank = ranksByPrecedence
+                    .computeIfAbsent(facetPrecedence, __->new 
FacetRank(facetType(), facetPrecedence));
+            rank.add(facet);
         }
 
         return changesTopRank;
     }
 
     public void addAll(final @NonNull FacetRanking facetRanking) {
-        facetRanking.facetsByPrecedence.asNavigableMapElseFail()
-            .forEach((k, facets)->facets.forEach(this::add));
+        facetRanking.ranksByPrecedence
+            .forEach((k, 
rank)->rank.facetsByQualifier().streamElements().forEach(this::add));
     }
 
     /**
@@ -132,12 +135,11 @@ public void addAll(final @NonNull FacetRanking 
facetRanking) {
      * @param facetType - for convenience, so the caller does not need to cast 
the result
      */
     public <F extends Facet> Optional<F> getWinner(
-            final @NonNull Class<F> facetType,
-            final @Nullable String qualifier) {
+            final @NonNull Class<F> facetType) {
         var eventFacet = getEventFacet(facetType);
-        if(eventFacet.isPresent())
-            return eventFacet;
-        return getWinnerNonEvent(facetType, qualifier);
+        return eventFacet.isPresent()
+            ? eventFacet
+            : getWinnerNonEvent(facetType);
     }
 
     /**
@@ -146,30 +148,11 @@ public <F extends Facet> Optional<F> getWinner(
      * @param facetType - for convenience, so the caller does not need to cast 
the result
      */
     public <F extends Facet> Optional<F> getWinnerNonEvent(
-            final @NonNull Class<F> facetType,
-            final @Nullable String qualifier) {
-        var topRank = getTopRank(facetType);
-        // all matching QualifiedFacet(s) take precedence
-        // all non-matching QualifiedFacet(s) must be ignored
-        // historically (initial design) the last one wins
-
-        F bestSoFar = null;
-
-        for(var facet : topRank) {
-            if(facet instanceof QualifiedFacet qFacet) {
-                if(Objects.equals(qualifier, qFacet.qualifier()))
-                    return Optional.of(facet);
-                else {
-                    continue;
-                }
-            }
-            bestSoFar = facet;
-        }
-
-        //FIXME if bestSoFar == null, then we have to go to the next lower 
rank;
-        //FIXME the getTopRank(facetType) logic is broken, because it depends 
now on context
-
-        return Optional.ofNullable(bestSoFar);
+            final @NonNull Class<F> facetType) {
+        var topRank = topRankInternal();
+        return topRank!=null
+                ? (Optional<F>) topRank.findBest(key())
+                : Optional.empty();
     }
 
     /**
@@ -180,14 +163,12 @@ public <F extends Facet> Optional<F> getWinnerNonEvent(
      */
     public <F extends Facet> Optional<F> getWinnerNonEventLowerOrEqualTo(
             final @NonNull Class<F> facetType,
-            final @NonNull Precedence precedenceUpper
-            //,
-            //final @Nullable String qualifier
-            ) {
+            final @NonNull Precedence precedenceUpper) {
+        var key = key();
         var selectedRank = getHighestPrecedenceLowerOrEqualTo(precedenceUpper);
         return selectedRank
-                .map(facetsByPrecedence::get)
-                .map(_Lists::lastElementIfAny) // historically the last one 
wins
+                .map(ranksByPrecedence::get)
+                .flatMap(rank->rank.findBest(key))
                 .map(_Casts::uncheckedCast);
     }
 
@@ -205,10 +186,12 @@ public <F extends Facet> Optional<F> getEventFacet(final 
@NonNull Class<F> facet
      */
     public <F extends Facet> Can<F> getTopRank(final @NonNull Class<F> 
facetType) {
         _Assert.assertEquals(this.facetType, facetType);
-        var topRankedFacets = 
facetsByPrecedence.asNavigableMapElseFail().lastEntry();
-        return topRankedFacets!=null
-                ? 
Can.<F>ofCollection(_Casts.uncheckedCast(topRankedFacets.getValue()))
-                : Can.empty();
+        var key = key();
+        for(var rank : ranksByPrecedence.descendingMap().values()) {
+            if(rank.hasBest(key))
+                return (Can<F>) rank.facetsMatching(key);
+        }
+        return Can.empty();
     }
 
     /**
@@ -221,12 +204,16 @@ public <F extends Facet> Can<F> getRankLowerOrEqualTo(
             final @NonNull Precedence precedenceUpper) {
         _Assert.assertEquals(this.facetType, facetType);
 
+        var key = key();
         var precedenceSelected = 
getHighestPrecedenceLowerOrEqualTo(precedenceUpper);
 
         return precedenceSelected
-        .map(facetsByPrecedence::get)
-        
.map(facetsOfSameRank->Can.<F>ofCollection(_Casts.uncheckedCast(facetsOfSameRank)))
-        .orElseGet(Can::empty);
+            .map(ranksByPrecedence::get)
+            .flatMap(rank->rank.findBest(key))
+
+
+            
.map(facetsOfSameRank->Can.<F>ofCollection(_Casts.uncheckedCast(facetsOfSameRank)))
+            .orElseGet(Can::empty);
     }
 
     /**
@@ -234,46 +221,51 @@ public <F extends Facet> Can<F> getRankLowerOrEqualTo(
      * @param precedenceUpper - upper bound
      */
     public Optional<Precedence> getHighestPrecedenceLowerOrEqualTo(final 
@NonNull Precedence precedenceUpper) {
-        return facetsByPrecedence
-        .keySet()
-        .stream()
-        .filter(precedence->precedence.ordinal()<=precedenceUpper.ordinal())
-        .max(Comparator.comparing(Precedence::ordinal));
+        var key = key();
+        for(var rank : ranksByPrecedence.descendingMap().values()) {
+            if(rank.precedence().ordinal()>precedenceUpper.ordinal()) {
+                continue; //next
+            }
+            if(rank.hasBest(key))
+                return Optional.of(rank.precedence());
+        }
+        return Optional.empty();
     }
 
     public Optional<Facet.Precedence> getTopPrecedence() {
-        return Optional.ofNullable(topPrecedenceRef.get());
+        var key = key();
+        return Optional.ofNullable(topPrecedenceRef.get(key));
     }
 
     // -- DYNAMIC UPDATE SUPPORT
 
-    /**
-     * Removes any facet of {@code facetType} from facetHolder if it passes 
the given {@code filter}.
-     * @param facetType - to ensure the filter is properly 
generic-type-constraint
-     * @param filter
-     */
-    public <F extends Facet> void purgeIf(
-            final @NonNull Class<F> facetType,
-            final @NonNull Predicate<? super F> filter) {
-
-        // reassess the top precedence
-        final _Reduction<Facet.Precedence> top = _Reduction.of(null, (a, 
b)->a==null?b:a.ordinal()>b.ordinal()?a:b);
-        var markedForRemoval = _Lists.newArrayList(facetsByPrecedence.size());
-
-        facetsByPrecedence.forEach((precedence, facets)->{
-            facets.removeIf(_Casts.uncheckedCast(filter));
-            if(!facets.isEmpty()) {
-                top.accept(precedence);
-            } else {
-                markedForRemoval.add(precedence);
-            }
-        });
-
-        topPrecedenceRef.set(top.getResult().orElse(null));
-
-        // remove keys that associate empty lists, so finding highest used 
precedence by key is simple
-        markedForRemoval.forEach(facetsByPrecedence::remove);
-    }
+//    /**
+//     * Removes any facet of {@code facetType} from facetHolder if it passes 
the given {@code filter}.
+//     * @param facetType - to ensure the filter is properly 
generic-type-constraint
+//     * @param filter
+//     */
+//    public <F extends Facet> void purgeIf(
+//            final @NonNull Class<F> facetType,
+//            final @NonNull Predicate<? super F> filter) {
+//
+//        // reassess the top precedence
+//        final _Reduction<Facet.Precedence> top = _Reduction.of(null, (a, 
b)->a==null?b:a.ordinal()>b.ordinal()?a:b);
+//        var markedForRemoval = 
_Lists.newArrayList(facetsByPrecedence.size());
+//
+//        facetsByPrecedence.forEach((precedence, facets)->{
+//            facets.removeIf(_Casts.uncheckedCast(filter));
+//            if(!facets.isEmpty()) {
+//                top.accept(precedence);
+//            } else {
+//                markedForRemoval.add(precedence);
+//            }
+//        });
+//
+//        topPrecedenceRef.set(top.getResult().orElse(null));
+//
+//        // remove keys that associate empty lists, so finding highest used 
precedence by key is simple
+//        markedForRemoval.forEach(facetsByPrecedence::remove);
+//    }
 
     // -- VALIDATION SUPPORT
 
@@ -302,7 +294,53 @@ public <F extends Facet> void visitTopRankPairs(
                 .skip(1)
                 .forEach(next->visitor.accept(firstOfTopRanking, next));
         }
+    }
+
+    // -- HELPER
 
+    QualifiedFacet.Key key() {
+        return new QualifiedFacet.Key(facetType, Facets.qualifier(null));
+    }
+
+    @Nullable
+    FacetRank topRankInternal() {
+        var key = key();
+        for(var rank : ranksByPrecedence.descendingMap().values()) {
+            if(rank.hasBest(key))
+                return rank;
+        }
+        return null;
+    }
+
+    /**
+     * Rules in order of strength:
+     * <ul>
+     * <li>all matching {@link QualifiedFacet}(s) take precedence</li>
+     * <li>all non-matching {@link QualifiedFacet}(s) must be ignored</li>
+     * <li>later take precedence over earlier</li>
+     * </ul>
+     */
+    @Deprecated
+    private static <F extends Facet> F findBestWithinRank(
+            final @NonNull Class<F> facetType,
+            final Can<? extends F> rank,
+            final @Nullable String qualifier) {
+
+        F bestNonQualified = null;
+        for(var it = rank.reverseIterator(); it.hasNext(); ) {
+            var facet = it.next();
+            if(facet instanceof QualifiedFacet qFacet) {
+                if(Objects.equals(qualifier, qFacet.qualifier()))
+                    return facet;
+                else {
+                    continue;
+                }
+            }
+            if(bestNonQualified==null) {
+                bestNonQualified = facet;
+            }
+        }
+        return bestNonQualified;
     }
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetUtil.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetUtil.java
index fd6af2ddcef..162b64b9a0e 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetUtil.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/FacetUtil.java
@@ -85,7 +85,7 @@ public static boolean addFacets(final @NonNull 
Iterable<Facet> facetList) {
 
     public static <T extends Facet> XmlSchema.ExtensionData<T> 
getFacetsByType(final FacetHolder facetHolder) {
 
-        return new XmlSchema.ExtensionData<T>() {
+        return new XmlSchema.ExtensionData<>() {
 
             @Override
             public int size() {
@@ -119,7 +119,7 @@ public static void updateFacet(final @Nullable Facet facet) 
{
                 .orElse(false);
         if(skip) return;
 
-        purgeIf(facet.facetType(), facet.getClass()::isInstance, 
facet.facetHolder());
+        //FIXME purgeIf(facet.facetType(), facet.getClass()::isInstance, 
facet.facetHolder());
         addFacet(facet);
     }
 
@@ -134,25 +134,25 @@ public static <F extends Facet> void updateFacetIfPresent(
         updateFacet(facetIfAny.orElse(null));
     }
 
-    /**
-     * Removes any facet of facet-type from facetHolder if it passes the given 
filter.
-     */
-    private static <F extends Facet> void purgeIf(
-            final Class<F> facetType,
-            final Predicate<? super F> filter,
-            final FacetHolder facetHolder) {
-
-        facetHolder.getFacetRanking(facetType)
-        .ifPresent(ranking->ranking.purgeIf(facetType, filter));
-    }
+//    /**
+//     * Removes any facet of facet-type from facetHolder if it passes the 
given filter.
+//     */
+//    private static <F extends Facet> void purgeIf(
+//            final Class<F> facetType,
+//            final Predicate<? super F> filter,
+//            final FacetHolder facetHolder) {
+//
+//        facetHolder.getFacetRanking(facetType)
+//            .ifPresent(ranking->ranking.purgeIf(facetType, filter));
+//    }
 
     // -- FACET ATTRIBUTES
 
     public static String attributesAsString(final Facet facet) {
         return streamAttributes(facet)
-                .filter(kv->!kv.key().equals("facet")) // skip superfluous 
attribute
-                .map(_Strings.KeyValuePair::toString)
-                .collect(Collectors.joining("; "));
+            .filter(kv->!kv.key().equals("facet")) // skip superfluous 
attribute
+            .map(_Strings.KeyValuePair::toString)
+            .collect(Collectors.joining("; "));
     }
 
     public static Stream<_Strings.KeyValuePair> streamAttributes(final Facet 
facet) {
@@ -167,8 +167,8 @@ public static String toString(final Facet facet) {
         var className = ClassUtils.getShortName(facet.getClass());
         var attributesAsString = attributesAsString(facet);
         return facet.getClass() == facet.facetType()
-                ? String.format("%s[%s]", className, attributesAsString)
-                : String.format("%s[type=%s; %s]", className, 
ClassUtils.getShortName(facet.facetType()), attributesAsString);
+            ? String.format("%s[%s]", className, attributesAsString)
+            : String.format("%s[type=%s; %s]", className, 
ClassUtils.getShortName(facet.facetType()), attributesAsString);
     }
 
     // -- FACET LOOKUP
@@ -176,14 +176,13 @@ public static String toString(final Facet facet) {
     /** Looks up specified facetType within given {@link FacetHolder}s, 
honoring Facet {@link Precedence},
      * while first one found wins over later found if they have the same 
precedence. */
     public static <F extends Facet> Optional<F> lookupFacetIn(final @NonNull 
Class<F> facetType, final FacetHolder ... facetHolders) {
-        if(facetHolders==null) {
+        if(facetHolders==null)
             return Optional.empty();
-        }
         return Stream.of(facetHolders)
-        .filter(_NullSafe::isPresent)
-        .map(facetHolder->facetHolder.getFacet(facetType))
-        .filter(_NullSafe::isPresent)
-        .reduce((a, b)->b.precedence().ordinal()>a.precedence().ordinal()
+            .filter(_NullSafe::isPresent)
+            .map(facetHolder->facetHolder.getFacet(facetType))
+            .filter(_NullSafe::isPresent)
+            .reduce((a, b)->b.precedence().ordinal()>a.precedence().ordinal()
                 ? b
                 : a);
     }
@@ -194,9 +193,8 @@ public static <F extends Facet> Optional<F> 
lookupFacetInButExcluding(
             final @NonNull Class<F> facetType,
             final Predicate<Object> excluded,
             final FacetHolder ... facetHolders) {
-        if(facetHolders==null) {
+        if(facetHolders==null)
             return Optional.empty();
-        }
         return Stream.of(facetHolders)
         .filter(_NullSafe::isPresent)
         .filter(x -> !excluded.test(x))
@@ -225,14 +223,13 @@ public static <E extends T, T extends Facet> Optional<E> 
computeIfAbsentExact(
         if(winnerFacet==null) return 
Optional.of(addFacet(facetFactory.apply(facetHolder)));
         if(winnerFacet.getClass().equals(facetExactClass)) return 
Optional.of(winnerFacet).map(facetExactClass::cast);
         // check if we are allowed to override based on precedence
-        
if(winnerFacet.precedence().ordinal()<=overrideUpToIncluding.ordinal()) {
+        if(winnerFacet.precedence().ordinal()<=overrideUpToIncluding.ordinal())
             return Optional.of(addFacet(facetFactory.apply(facetHolder)));
-        }
         // not allowed to override
         return Optional.empty();
     }
 
-       public static void visitAttributes(Facet facet, BiConsumer<String, 
Object> visitor) {
+       public static void visitAttributes(final Facet facet, final 
BiConsumer<String, Object> visitor) {
         visitor.accept("facet", ClassUtils.getShortName(facet.getClass()));
         visitor.accept("precedence", facet.precedence().name());
 
@@ -243,8 +240,8 @@ public static void visitAttributes(Facet facet, 
BiConsumer<String, Object> visit
             visitor.accept("interactionAdvisors", interactionAdvisors);
         }
        }
-       
-    private String interactionAdvisors(Facet facet, final String delimiter) {
+
+    private String interactionAdvisors(final Facet facet, final String 
delimiter) {
         return Stream.of(Validating.class, HidingOrShowing.class, 
DisablingOrEnabling.class)
                .filter(marker->marker.isAssignableFrom(facet.getClass()))
                .map(Class::getSimpleName)
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/QualifiedFacet.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/QualifiedFacet.java
index ae81b42478b..9bf3a014da8 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/QualifiedFacet.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facetapi/QualifiedFacet.java
@@ -2,6 +2,8 @@
 
 import org.jspecify.annotations.Nullable;
 
+import org.apache.causeway.commons.internal.base._Strings;
+
 /**
  * A {@link Facet} can be qualified (similar to Spring beans) in order to 
allow for alternative
  * behavior or semantics based on context.
@@ -20,6 +22,39 @@
 @FunctionalInterface
 public interface QualifiedFacet {
 
+    record Key(
+            Class<? extends Facet> facetType,
+            /**
+             * The empty String "" is used for qualified facets, that have an 
empty qualifier
+             */
+            @Nullable String qualifier) {
+
+        static Key unqualified(final Class<? extends Facet> facetType) {
+            return new Key(facetType, null);
+        }
+        static Key forFacet(final Facet facet) {
+            return facet instanceof QualifiedFacet qFacet
+                    ? new Key(facet.facetType(), 
_Strings.nullToEmpty(qFacet.qualifier()))
+                    : unqualified(facet.facetType());
+        }
+        public boolean isQualified() {
+            return qualifier!=null;
+        }
+        public boolean isUnqualified() {
+            return qualifier==null;
+        }
+        public Key toUnqualified() {
+            return isUnqualified()
+                ? this
+                : unqualified(facetType);
+        }
+        public Key toQualified() {
+            return isQualified()
+                ? this
+                : new Key(facetType, "");
+        }
+    }
+
     @Nullable String qualifier();
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/CssClassFacetViaDomainObjectLayoutAnnotationUsingCssClassUiEvent.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/CssClassFacetViaDomainObjectLayoutAnnotationUsingCssClassUiEvent.java
index 8466e3e8564..6c5fd159376 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/CssClassFacetViaDomainObjectLayoutAnnotationUsingCssClassUiEvent.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/CssClassFacetViaDomainObjectLayoutAnnotationUsingCssClassUiEvent.java
@@ -32,7 +32,6 @@
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
 import org.apache.causeway.core.metamodel.object.MmEventUtils;
 import 
org.apache.causeway.core.metamodel.services.events.MetamodelEventService;
-import org.apache.causeway.core.metamodel.util.Facets;
 
 public class CssClassFacetViaDomainObjectLayoutAnnotationUsingCssClassUiEvent
 extends CssClassFacetAbstract {
@@ -82,9 +81,8 @@ public String cssClass(final ManagedObject owningAdapter) {
 
         if(cssClass == null) {
             // ie no subscribers out there...
-            final String qualifier = Facets.qualifier(facetHolder());
             final CssClassFacet underlyingCssClassFacet = 
getSharedFacetRanking()
-                
.flatMap(facetRanking->facetRanking.getWinnerNonEvent(CssClassFacet.class, 
qualifier))
+                
.flatMap(facetRanking->facetRanking.getWinnerNonEvent(CssClassFacet.class))
                 .orElse(null);
 
             if(underlyingCssClassFacet!=null)
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/IconFacetViaDomainObjectLayoutAnnotationUsingIconUiEvent.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/IconFacetViaDomainObjectLayoutAnnotationUsingIconUiEvent.java
index d03e655031b..0d8abddea89 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/IconFacetViaDomainObjectLayoutAnnotationUsingIconUiEvent.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/IconFacetViaDomainObjectLayoutAnnotationUsingIconUiEvent.java
@@ -33,7 +33,6 @@
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
 import org.apache.causeway.core.metamodel.object.MmEventUtils;
 import 
org.apache.causeway.core.metamodel.services.events.MetamodelEventService;
-import org.apache.causeway.core.metamodel.util.Facets;
 
 public record IconFacetViaDomainObjectLayoutAnnotationUsingIconUiEvent(
     Class<? extends IconUiEvent<Object>> iconUiEventClass,
@@ -96,9 +95,8 @@ private IconUiEvent<Object> newIconUiEvent(final 
ManagedObject owningAdapter, fi
     }
 
     private Optional<IconFacet> underlyingIconFacet() {
-        final String qualifier = Facets.qualifier(facetHolder());
         return getSharedFacetRanking()
-            
.flatMap(facetRanking->facetRanking.getWinnerNonEvent(IconFacet.class, 
qualifier));
+            
.flatMap(facetRanking->facetRanking.getWinnerNonEvent(IconFacet.class));
     }
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/TitleFacetViaDomainObjectLayoutAnnotationUsingTitleUiEvent.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/TitleFacetViaDomainObjectLayoutAnnotationUsingTitleUiEvent.java
index 4a0e0cc4dad..86864b0614e 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/TitleFacetViaDomainObjectLayoutAnnotationUsingTitleUiEvent.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/TitleFacetViaDomainObjectLayoutAnnotationUsingTitleUiEvent.java
@@ -37,7 +37,6 @@
 import org.apache.causeway.core.metamodel.object.MmEventUtils;
 import 
org.apache.causeway.core.metamodel.services.events.MetamodelEventService;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
-import org.apache.causeway.core.metamodel.util.Facets;
 
 public class TitleFacetViaDomainObjectLayoutAnnotationUsingTitleUiEvent
 extends TitleFacetAbstract {
@@ -99,10 +98,8 @@ public String title(final TitleRenderRequest 
titleRenderRequest) {
         if(titleUiEvent.getTitle() == null
                 && titleUiEvent.getTranslatableTitle() == null) {
             // ie no subscribers out there...
-
-            final String qualifier = Facets.qualifier(facetHolder());
             final TitleFacet underlyingTitleFacet = getSharedFacetRanking()
-                
.flatMap(facetRanking->facetRanking.getWinnerNonEvent(TitleFacet.class, 
qualifier))
+                
.flatMap(facetRanking->facetRanking.getWinnerNonEvent(TitleFacet.class))
                 .orElse(null);
 
             if(underlyingTitleFacet!=null)
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/layout/LayoutPrefixFacetForUiEvent.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/layout/LayoutPrefixFacetForUiEvent.java
index 37b5a9ddd70..d6650a98176 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/layout/LayoutPrefixFacetForUiEvent.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/layout/LayoutPrefixFacetForUiEvent.java
@@ -79,7 +79,7 @@ public String layoutPrefix(final ManagedObject managedObject) 
{
 
         // ie no subscribers out there, then fallback to the underlying ...
         return getSharedFacetRanking()
-            
.flatMap(facetRanking->facetRanking.getWinnerNonEvent(LayoutPrefixFacet.class, 
null))
+            
.flatMap(facetRanking->facetRanking.getWinnerNonEvent(LayoutPrefixFacet.class))
             
.map(underlyingLayoutFacet->underlyingLayoutFacet.layoutPrefix(managedObject))
             .orElse(null);
     }
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 b19018ea672..6b88f44e868 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
@@ -55,7 +55,6 @@
 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.apache.causeway.core.metamodel.util.Facets;
 
 /**
  * @since 2.0
@@ -99,12 +98,10 @@ public void validateObjectEnter(final ObjectSpecification 
spec) {
             .map(MethodFacade::asMethodForIntrospection)
             .forEach(memberMethods::add);
 
-        final String qualifier = Facets.qualifier(spec);
-
         spec
             .streamFacetHolders()
             .flatMap(FacetHolder::streamFacetRankings)
-            
.map(facetRanking->facetRanking.getWinnerNonEvent(facetRanking.facetType(), 
qualifier))
+            
.map(facetRanking->facetRanking.getWinnerNonEvent(facetRanking.facetType()))
             .flatMap(Optional::stream)
             .filter(ImperativeFacet.class::isInstance)
             .map(ImperativeFacet.class::cast)
diff --git 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingTest.java
 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingTest.java
index 56b534d83ff..be4cb6adca1 100644
--- 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingTest.java
+++ 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingTest.java
@@ -98,7 +98,7 @@ void customNamed() {
 
         // verify winning facet is the same object as the last one added from 
latest layout.xml reload,
         // to make sure we are not feed the winner from an outdated cache
-        assertSame(facetRanking.getWinnerNonEvent(MemberNamedFacet.class, 
null).get(), xmlFacetRank.getLastElseFail());
+        
assertSame(facetRanking.getWinnerNonEvent(MemberNamedFacet.class).get(), 
xmlFacetRank.getLastElseFail());
 
     }
 
diff --git 
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/menubars/bootstrap/MenuBarsServiceBSTest.java
 
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/menubars/bootstrap/MenuBarsServiceBSTest.java
index 36e5a1f2261..ccad2ab50e8 100644
--- 
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/menubars/bootstrap/MenuBarsServiceBSTest.java
+++ 
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/menubars/bootstrap/MenuBarsServiceBSTest.java
@@ -39,7 +39,6 @@
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
 import org.apache.causeway.core.metamodel.facetapi.Facet.Precedence;
 import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet;
-import org.apache.causeway.core.metamodel.util.Facets;
 import 
org.apache.causeway.core.mmtestsupport.MetaModelContext_forTesting.MetaModelContext_forTestingBuilder;
 import org.apache.causeway.core.runtimeservices.RuntimeServicesTestAbstract;
 
@@ -159,11 +158,9 @@ void customNamed() {
         // verify rank did not grow with latest menubars.xml reload
         assertEquals(1, xmlFacetRank.size());
 
-        final String qualifier = Facets.qualifier(objectAction);
-
         // verify winning facet is the same object as the last one added from 
latest menubars.xml reload,
         // to make sure we are not feed the winner from an outdated cache
-        assertSame(facetRanking.getWinnerNonEvent(MemberNamedFacet.class, 
qualifier).get(), xmlFacetRank.getLastElseFail());
+        
assertSame(facetRanking.getWinnerNonEvent(MemberNamedFacet.class).get(), 
xmlFacetRank.getLastElseFail());
     }
 
     // -- HELPER


Reply via email to