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

lprimak pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/shiro.git

commit b06d2b4905edd1cdc3e47ef829ed4a16f3fe952c
Author: Martin Geisse <[email protected]>
AuthorDate: Thu Jul 11 08:18:42 2024 +0200

    GitHub issue #1548. Declare PrincipalCollection interface to be intended to 
be immutable; provide an immutable implementation including a builder class; 
deprecate the mutable implementation; use the immutable one in 
SimpleAuthenticationInfo for merging.
    
    Background: Merging AuthenticationInfo and the contained 
PrincipalCollections previously lead to one of the involved 
PrincipalCollections be selected by undefined means and being mutated, 
propagating the changes to other callers that did not expect such changes, 
including the authentication cache.
---
 .../shiro/authc/SimpleAuthenticationInfo.java      |  10 +-
 .../subject/ImmutablePrincipalCollection.java      | 340 +++++++++++++++++++++
 .../shiro/subject/MutablePrincipalCollection.java  |   3 +
 .../apache/shiro/subject/PrincipalCollection.java  |   4 +
 .../shiro/subject/SimplePrincipalCollection.java   |   7 +
 .../subject/ImmutablePrincipalCollectionTest.java  | 228 ++++++++++++++
 6 files changed, 587 insertions(+), 5 deletions(-)

diff --git 
a/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java 
b/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java
index 8cae62fbe..048bdeb9c 100644
--- a/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java
+++ b/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java
@@ -20,7 +20,7 @@ package org.apache.shiro.authc;
 
 import org.apache.shiro.lang.util.ByteSource;
 import org.apache.shiro.lang.util.SimpleByteSource;
-import org.apache.shiro.subject.MutablePrincipalCollection;
+import org.apache.shiro.subject.ImmutablePrincipalCollection;
 import org.apache.shiro.subject.PrincipalCollection;
 import org.apache.shiro.subject.SimplePrincipalCollection;
 
@@ -205,10 +205,10 @@ public class SimpleAuthenticationInfo implements 
MergableAuthenticationInfo, Sal
         if (this.principals == null) {
             this.principals = info.getPrincipals();
         } else {
-            if (!(this.principals instanceof MutablePrincipalCollection)) {
-                this.principals = new 
SimplePrincipalCollection(this.principals);
-            }
-            ((MutablePrincipalCollection) 
this.principals).addAll(info.getPrincipals());
+            this.principals = new ImmutablePrincipalCollection.Builder()
+                    .addPrincipals(this.principals)
+                    .addPrincipals(info.getPrincipals())
+                    .build();
         }
 
         //only mess with a salt value if we don't have one yet.  It doesn't 
make sense
diff --git 
a/core/src/main/java/org/apache/shiro/subject/ImmutablePrincipalCollection.java 
b/core/src/main/java/org/apache/shiro/subject/ImmutablePrincipalCollection.java
new file mode 100644
index 000000000..d0dde8dbf
--- /dev/null
+++ 
b/core/src/main/java/org/apache/shiro/subject/ImmutablePrincipalCollection.java
@@ -0,0 +1,340 @@
+/*
+ * 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.shiro.subject;
+
+import org.apache.shiro.lang.util.StringUtils;
+import org.apache.shiro.util.CollectionUtils;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.List;
+import java.util.Map;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.HashSet;
+import java.util.Objects;
+
+
+/**
+ * An immutable implementation of the {@link PrincipalCollection} interface 
that tracks principals internally
+ * by storing them in a {@link LinkedHashMap} of {@link LinkedHashSet}s, 
preserving the order of realms and the order
+ * of principals per realm and all wrapped to become unmodifiable.
+ * <p>
+ * The first principal of the first non-empty realm is considered the primary 
principal of this collection.
+ */
+@SuppressWarnings("unchecked")
+public final class ImmutablePrincipalCollection implements PrincipalCollection 
{
+
+    /**
+     * Shared empty instance to avoid per-instance allocation overhead.
+     */
+    public static final ImmutablePrincipalCollection EMPTY = empty();
+
+    // Serialization reminder:
+    // You _MUST_ change this number if you introduce a change to this class
+    // that is NOT serialization backwards compatible.  
Serialization-compatible
+    // changes do not require a change to this number.  If you need to generate
+    // a new number in this case, use the JDK's 'serialver' program to 
generate it.
+    private static final long serialVersionUID = 8095033991024104940L;
+
+    // Set of principals per realm
+    private final Map<String, Set<Object>> realmPrincipals;
+
+    // Cached toString() result, as this can be printed many times in logging. 
While this is formally a mutable
+    // variable, its state is not visible to callers of this class.
+    private transient String cachedToString;
+
+    /**
+     * This private constructor does not copy the argument collections, nor 
does it ensure immutability. The factory
+     * methods do that, and we just store the collections here to avoid double 
copying.
+     */
+    private ImmutablePrincipalCollection(Map<String, Set<Object>> 
realmPrincipals) {
+        if (realmPrincipals == null) {
+            throw new IllegalArgumentException("realmPrincipals argument 
cannot be null.");
+        }
+        this.realmPrincipals = realmPrincipals;
+    }
+
+    // order-preserving variant of Set.copyOf()
+    private static <T> Set<T> copySetShallow(Collection<? extends T> set) {
+        return Collections.unmodifiableSet(new LinkedHashSet<>(set));
+    }
+
+    // order-preserving variant of Map.copyOf()
+    private static <K, V> Map<K, V> copyMapShallow(Map<? extends K, ? extends 
V> map) {
+        return Collections.unmodifiableMap(new LinkedHashMap<>(map));
+    }
+
+    /**
+     * Creates a new, empty {@code ImmutablePrincipalCollection} instance.
+     *
+     * @return the new instance
+     */
+    public static ImmutablePrincipalCollection empty() {
+        return new ImmutablePrincipalCollection(Collections.emptyMap());
+    }
+
+    /**
+     * Creates a new {@code ImmutablePrincipalCollection} instance with the 
specified principals all
+     * belonging to the same realm.
+     *
+     * @param principals the principals to add
+     * @param realmName the name of the realm to add the principals to
+     * @return the new instance
+     */
+    public static ImmutablePrincipalCollection ofSingleRealm(Collection<?> 
principals, String realmName) {
+        if (principals.isEmpty()) {
+            return empty();
+        } else {
+            return new 
ImmutablePrincipalCollection(Collections.singletonMap(realmName, 
copySetShallow(principals)));
+        }
+    }
+
+    /**
+     * Creates a new {@code ImmutablePrincipalCollection} instance with the 
specified single principal
+     * for a single realm.
+     *
+     * @param principal the principal to add
+     * @param realmName the name of the realm to add the principal to
+     * @return the new instance
+     */
+    public static ImmutablePrincipalCollection ofSinglePrincipal(Object 
principal, String realmName) {
+        return new 
ImmutablePrincipalCollection(Collections.singletonMap(realmName, 
Set.of(principal)));
+    }
+
+    /**
+     * Creates a new {@code ImmutablePrincipalCollection} instance with the 
realms and principals from another
+     * collection added in iteration order.
+     *
+     * @param original the original collection to copy
+     * @return the new instance
+     */
+    public static ImmutablePrincipalCollection copyOf(PrincipalCollection 
original) {
+        if (original instanceof ImmutablePrincipalCollection) {
+            return (ImmutablePrincipalCollection) original;
+        }
+        return new Builder().addPrincipals(original).build();
+    }
+
+    @Override
+    public Object getPrimaryPrincipal() {
+        Iterator<?> iterator = iterator();
+        return iterator.hasNext() ? iterator.next() : null;
+    }
+
+    @Override
+    public <T> T oneByType(Class<T> type) {
+        for (Set<?> set : realmPrincipals.values()) {
+            for (Object principal : set) {
+                if (type.isAssignableFrom(principal.getClass())) {
+                    return (T) principal;
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public <T> Set<T> byType(Class<T> type) {
+        Set<T> typed = new LinkedHashSet<>();
+        for (Set<?> set : realmPrincipals.values()) {
+            for (Object principal : set) {
+                if (type.isAssignableFrom(principal.getClass())) {
+                    typed.add((T) principal);
+                }
+            }
+        }
+        return Set.copyOf(typed);
+    }
+
+    @Override
+    public List<?> asList() {
+        return List.copyOf(asSet());
+    }
+
+    @Override
+    public Set<?> asSet() {
+        Set<Object> result = new HashSet<>();
+        for (Set<?> set : realmPrincipals.values()) {
+            result.addAll(set);
+        }
+        return copySetShallow(result);
+    }
+
+    @Override
+    public Set<?> fromRealm(String realmName) {
+        Set<?> principals = realmPrincipals.get(realmName);
+        return principals != null ? principals : Collections.emptySet();
+    }
+
+    @Override
+    public Set<String> getRealmNames() {
+        return realmPrincipals.keySet();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        for (Set<?> principals : realmPrincipals.values()) {
+            if (!principals.isEmpty()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public Iterator<?> iterator() {
+        return asSet().iterator();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (o instanceof ImmutablePrincipalCollection) {
+            ImmutablePrincipalCollection other = 
(ImmutablePrincipalCollection) o;
+            return Objects.equals(this.realmPrincipals, other.realmPrincipals);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return realmPrincipals.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        if (this.cachedToString == null) {
+            Set<?> principals = asSet();
+            if (!CollectionUtils.isEmpty(principals)) {
+                this.cachedToString = 
StringUtils.toString(principals.toArray());
+            } else {
+                this.cachedToString = "empty";
+            }
+        }
+        return this.cachedToString;
+    }
+
+    /**
+     * Builder to create new {@link ImmutablePrincipalCollection} instances 
for more complex cases than a single realm.
+     */
+    public static final class Builder {
+
+        private final Map<String, Set<Object>> realmPrincipals = new 
LinkedHashMap<>();
+
+        /**
+         * Returns the map of realm names to principal sets.
+         *
+         * @return the map-of-sets of principals for all realms
+         */
+        public Map<String, Set<Object>> getPrincipalsForAllRealms() {
+            return realmPrincipals;
+        }
+
+        /**
+         * Returns the set of principals for the specified realm. If the realm 
does not yet exist in this builder, it is
+         * added to the end of the list of realms.
+         *
+         * @param realmName the realm to get the set of principals for
+         * @return the set of principals for that realm
+         */
+        public Set<Object> getPrincipalsForRealm(String realmName) {
+            if (realmName == null) {
+                throw new NullPointerException("realmName argument cannot be 
null.");
+            }
+            return realmPrincipals.computeIfAbsent(realmName, (_key) -> new 
LinkedHashSet<>());
+        }
+
+        /**
+         * Adds a single principal to a realm. The principal is added to the 
end of the list of principals for that
+         * realm. If the realm does not yet exist in this builder, it is added 
to the end of the list of realms.
+         *
+         * @param principal the principal to add
+         * @param realmName the realm to add the principal to
+         * @return this
+         */
+        public Builder addPrincipal(Object principal, String realmName) {
+            if (principal == null) {
+                throw new NullPointerException("principal argument cannot be 
null.");
+            }
+            if (realmName == null) {
+                throw new NullPointerException("realmName argument cannot be 
null.");
+            }
+            getPrincipalsForRealm(realmName).add(principal);
+            return this;
+        }
+
+        /**
+         * Adds a collection of principals to a single realm. The principals 
are stored in the iteration order of the
+         * argument collection, appended to the end of the principals already 
present for that realm. If the realm does
+         * not yet exist in this builder, it is added to the end of the list 
of realms.
+         *
+         * @param principals the principals to add
+         * @param realmName the name of the realm to add the principals to
+         * @return this
+         */
+        public Builder addPrincipals(Collection<?> principals, String 
realmName) {
+            if (principals == null) {
+                throw new NullPointerException("principals argument cannot be 
null.");
+            }
+            for (Object principal : principals) {
+                addPrincipal(principal, realmName);
+            }
+            return this;
+        }
+
+        /**
+         * Adds all principals from the specified {@link PrincipalCollection} 
to this builder. The order of realms,
+         * as well as the order of principals within each realm, is preserved.
+         *
+         * @param principals the principals to add
+         * @return this
+         */
+        public Builder addPrincipals(PrincipalCollection principals) {
+            if (principals == null) {
+                throw new NullPointerException("principals argument cannot be 
null.");
+            }
+            for (String realmName : principals.getRealmNames()) {
+                addPrincipals(principals.fromRealm(realmName), realmName);
+            }
+            return this;
+        }
+
+        /**
+         * Builds an {@link ImmutablePrincipalCollection} from the current 
state of this builder.
+         *
+         * @return the finished principal collection
+         */
+        public ImmutablePrincipalCollection build() {
+            Map<String, Set<Object>> copy = new LinkedHashMap<>();
+            for (Map.Entry<String, Set<Object>> entry : 
realmPrincipals.entrySet()) {
+                if (!entry.getValue().isEmpty()) {
+                    copy.put(entry.getKey(), copySetShallow(entry.getValue()));
+                }
+            }
+            return new ImmutablePrincipalCollection(copyMapShallow(copy));
+        }
+
+    }
+
+}
diff --git 
a/core/src/main/java/org/apache/shiro/subject/MutablePrincipalCollection.java 
b/core/src/main/java/org/apache/shiro/subject/MutablePrincipalCollection.java
index 251b69954..8b18e7fb7 100644
--- 
a/core/src/main/java/org/apache/shiro/subject/MutablePrincipalCollection.java
+++ 
b/core/src/main/java/org/apache/shiro/subject/MutablePrincipalCollection.java
@@ -25,6 +25,9 @@ import java.util.Collection;
  * A {@link PrincipalCollection} that allows modification.
  *
  * @since 0.9
+ * @deprecated Principal collections should not be mutable after their initial 
construction because they are shared by
+ * an unspecified and unpredictable number of other objects. This interface 
only exists for compatibility with existing
+ * code.
  */
 public interface MutablePrincipalCollection extends PrincipalCollection {
 
diff --git 
a/core/src/main/java/org/apache/shiro/subject/PrincipalCollection.java 
b/core/src/main/java/org/apache/shiro/subject/PrincipalCollection.java
index 1de2cc3ad..1f760288a 100644
--- a/core/src/main/java/org/apache/shiro/subject/PrincipalCollection.java
+++ b/core/src/main/java/org/apache/shiro/subject/PrincipalCollection.java
@@ -35,6 +35,10 @@ import java.util.Set;
  * A PrincipalCollection organizes its internal principals based on the {@code 
Realm} where they came from when the
  * Subject was first created.  To obtain the principal(s) for a specific 
Realm, see the {@link #fromRealm} method.  You
  * can also see which realms contributed to this collection via the {@link 
#getRealmNames() getRealmNames()} method.
+ * <p/>
+ * Principal collections are intended to be immutable after their initial 
construction because they are shared by an
+ * unspecified and unpredictable number of other objects. If the 
implementation does allow mutation, that should be
+ * restricted to the time before the collection is shared with any other 
object.
  *
  * @see #getPrimaryPrincipal()
  * @see #fromRealm(String realmName)
diff --git 
a/core/src/main/java/org/apache/shiro/subject/SimplePrincipalCollection.java 
b/core/src/main/java/org/apache/shiro/subject/SimplePrincipalCollection.java
index daf47e81a..fbd57017f 100644
--- a/core/src/main/java/org/apache/shiro/subject/SimplePrincipalCollection.java
+++ b/core/src/main/java/org/apache/shiro/subject/SimplePrincipalCollection.java
@@ -38,6 +38,13 @@ import java.util.Set;
 /**
  * A simple implementation of the {@link MutablePrincipalCollection} interface 
that tracks principals internally
  * by storing them in a {@link LinkedHashMap}.
+ * <p/>
+ * To comply with the {@link PrincipalCollection PrincipalCollection} 
interface, mutation of a principal
+ * collection must be restricted to the time before it is shared with other 
objects.
+ *
+ * @deprecated use {@link ImmutablePrincipalCollection} instead. Principal 
collections should not be mutable after
+ * their initial construction because they are shared by an unspecified and 
unpredictable number of other objects.
+ * This implementation only exists for compatibility with existing code.
  *
  * @since 0.9
  */
diff --git 
a/core/src/test/java/org/apache/shiro/subject/ImmutablePrincipalCollectionTest.java
 
b/core/src/test/java/org/apache/shiro/subject/ImmutablePrincipalCollectionTest.java
new file mode 100644
index 000000000..0ea7fd39c
--- /dev/null
+++ 
b/core/src/test/java/org/apache/shiro/subject/ImmutablePrincipalCollectionTest.java
@@ -0,0 +1,228 @@
+/*
+ * 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.shiro.subject;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class ImmutablePrincipalCollectionTest {
+
+    private static final String MY_REALM = "myRealm";
+    private static final String OTHER_REALM = "otherRealm";
+
+    private static final String MY_PRINCIPAL = "myPrincipal";
+    private static final String OTHER_PRINCIPAL = "otherPrincipal";
+    private static final String THIRD_PRINCIPAL = "thirdPrincipal";
+
+    private void testEmptyHelper(PrincipalCollection c) {
+        assertNull(c.getPrimaryPrincipal());
+        assertNull(c.oneByType(Object.class));
+        assertNull(c.oneByType(String.class));
+        assertNull(c.oneByType(Integer.class));
+        assertTrue(c.byType(Object.class).isEmpty());
+        assertTrue(c.asSet().isEmpty());
+        assertTrue(c.asList().isEmpty());
+        assertTrue(c.fromRealm(MY_REALM).isEmpty());
+        assertTrue(c.getRealmNames().isEmpty());
+        assertTrue(c.isEmpty());
+    }
+
+    @Test
+    void testSharedEmpty() {
+        testEmptyHelper(ImmutablePrincipalCollection.EMPTY);
+    }
+
+    @Test
+    void testNewEmpty() {
+        testEmptyHelper(ImmutablePrincipalCollection.empty());
+    }
+
+    @Test
+    void testNewEmptyFromBuilder() {
+        testEmptyHelper(new ImmutablePrincipalCollection.Builder().build());
+    }
+
+    private void testSinglePrincipalHelper(PrincipalCollection c) {
+        assertEquals(MY_PRINCIPAL, c.getPrimaryPrincipal());
+
+        assertEquals(MY_PRINCIPAL, c.oneByType(Object.class));
+        assertEquals(MY_PRINCIPAL, c.oneByType(String.class));
+        assertNull(c.oneByType(Integer.class));
+
+        assertEquals(Set.of(MY_PRINCIPAL), c.byType(Object.class));
+        assertEquals(Set.of(MY_PRINCIPAL), c.byType(String.class));
+        assertEquals(Set.of(), c.byType(Integer.class));
+
+        assertEquals(Set.of(MY_PRINCIPAL), c.asSet());
+        assertEquals(List.of(MY_PRINCIPAL), c.asList());
+
+        assertEquals(Set.of(MY_PRINCIPAL), c.fromRealm(MY_REALM));
+        assertTrue(c.fromRealm(OTHER_REALM).isEmpty());
+
+        assertEquals(Set.of(MY_REALM), c.getRealmNames());
+        assertFalse(c.isEmpty());
+    }
+
+    @Test
+    void testNewSinglePrincipal() {
+        
testSinglePrincipalHelper(ImmutablePrincipalCollection.ofSinglePrincipal(MY_PRINCIPAL,
 MY_REALM));
+    }
+
+    @Test
+    void testNewSinglePrincipalUsingCollection() {
+        
testSinglePrincipalHelper(ImmutablePrincipalCollection.ofSingleRealm(List.of(MY_PRINCIPAL),
 MY_REALM));
+    }
+
+    @Test
+    void testNewSinglePrincipalUsingBuilder() {
+        ImmutablePrincipalCollection collection = new 
ImmutablePrincipalCollection.Builder()
+                .addPrincipal(MY_PRINCIPAL, MY_REALM)
+                .build();
+        testSinglePrincipalHelper(collection);
+    }
+
+    @Test
+    void testNewSinglePrincipalUsingBuilderAndCollection() {
+        ImmutablePrincipalCollection collection = new 
ImmutablePrincipalCollection.Builder()
+                .addPrincipals(List.of(MY_PRINCIPAL), MY_REALM)
+                .build();
+        testSinglePrincipalHelper(collection);
+    }
+
+    private void testSingleRealmMultiplePrincipalsHelper(PrincipalCollection 
c) {
+        assertEquals(MY_PRINCIPAL, c.getPrimaryPrincipal());
+
+        assertEquals(MY_PRINCIPAL, c.oneByType(Object.class));
+        assertEquals(MY_PRINCIPAL, c.oneByType(String.class));
+        assertNull(c.oneByType(Integer.class));
+
+        assertEquals(Set.of(MY_PRINCIPAL, OTHER_PRINCIPAL), 
c.byType(Object.class));
+        assertEquals(Set.of(MY_PRINCIPAL, OTHER_PRINCIPAL), 
c.byType(String.class));
+        assertEquals(Set.of(), c.byType(Integer.class));
+
+        assertEquals(Set.of(MY_PRINCIPAL, OTHER_PRINCIPAL), c.asSet());
+        assertEquals(List.of(MY_PRINCIPAL, OTHER_PRINCIPAL), c.asList());
+
+        assertEquals(Set.of(MY_PRINCIPAL, OTHER_PRINCIPAL), 
c.fromRealm(MY_REALM));
+        assertTrue(c.fromRealm(OTHER_REALM).isEmpty());
+
+        assertEquals(Set.of(MY_REALM), c.getRealmNames());
+        assertFalse(c.isEmpty());
+    }
+
+    @Test
+    void testNewSingleRealmMultiplePrincipalsUsingCollection() {
+        testSingleRealmMultiplePrincipalsHelper(
+                
ImmutablePrincipalCollection.ofSingleRealm(List.of(MY_PRINCIPAL, 
OTHER_PRINCIPAL), MY_REALM));
+    }
+
+    @Test
+    void testNewSingleRealmMultiplePrincipalsUsingBuilderAndCollection() {
+        ImmutablePrincipalCollection collection = new 
ImmutablePrincipalCollection.Builder()
+                .addPrincipals(List.of(MY_PRINCIPAL, OTHER_PRINCIPAL), 
MY_REALM)
+                .build();
+        testSingleRealmMultiplePrincipalsHelper(collection);
+    }
+
+    @Test
+    void testNewSingleRealmMultiplePrincipalsUsingBuilderAndMultipleCalls() {
+        ImmutablePrincipalCollection collection = new 
ImmutablePrincipalCollection.Builder()
+                .addPrincipal(MY_PRINCIPAL, MY_REALM)
+                .addPrincipal(OTHER_PRINCIPAL, MY_REALM)
+                .build();
+        testSingleRealmMultiplePrincipalsHelper(collection);
+    }
+
+    private void 
testMultipleRealmsSinglePrincipalEachHelper(PrincipalCollection c) {
+        assertEquals(MY_PRINCIPAL, c.getPrimaryPrincipal());
+
+        assertEquals(MY_PRINCIPAL, c.oneByType(Object.class));
+        assertEquals(MY_PRINCIPAL, c.oneByType(String.class));
+        assertNull(c.oneByType(Integer.class));
+
+        assertEquals(Set.of(MY_PRINCIPAL, OTHER_PRINCIPAL), 
c.byType(Object.class));
+        assertEquals(Set.of(MY_PRINCIPAL, OTHER_PRINCIPAL), 
c.byType(String.class));
+        assertEquals(Set.of(), c.byType(Integer.class));
+
+        assertEquals(Set.of(MY_PRINCIPAL, OTHER_PRINCIPAL), c.asSet());
+        assertEquals(List.of(MY_PRINCIPAL, OTHER_PRINCIPAL), c.asList());
+
+        assertEquals(Set.of(MY_PRINCIPAL), c.fromRealm(MY_REALM));
+        assertEquals(Set.of(OTHER_PRINCIPAL), c.fromRealm(OTHER_REALM));
+
+        assertEquals(Set.of(MY_REALM, OTHER_REALM), c.getRealmNames());
+        assertFalse(c.isEmpty());
+    }
+
+    @Test
+    void testNewMultipleRealmsSinglePrincipalEachUsingBuilder() {
+        ImmutablePrincipalCollection collection = new 
ImmutablePrincipalCollection.Builder()
+                .addPrincipal(MY_PRINCIPAL, MY_REALM)
+                .addPrincipal(OTHER_PRINCIPAL, OTHER_REALM)
+                .build();
+        testMultipleRealmsSinglePrincipalEachHelper(collection);
+    }
+
+    @Test
+    void 
testNewMultipleRealmsSinglePrincipalEachUsingBuilderAndMultipleCalls() {
+        ImmutablePrincipalCollection collection = new 
ImmutablePrincipalCollection.Builder()
+                .addPrincipals(List.of(MY_PRINCIPAL), MY_REALM)
+                .addPrincipals(List.of(OTHER_PRINCIPAL), OTHER_REALM)
+                .build();
+        testMultipleRealmsSinglePrincipalEachHelper(collection);
+    }
+
+    @Test
+    void testComplexScenario() {
+        ImmutablePrincipalCollection c = new 
ImmutablePrincipalCollection.Builder()
+                .addPrincipal(MY_PRINCIPAL, MY_REALM)
+                .addPrincipal(OTHER_PRINCIPAL, OTHER_REALM)
+                .addPrincipal(THIRD_PRINCIPAL, MY_REALM)
+                .build();
+
+        assertEquals(MY_PRINCIPAL, c.getPrimaryPrincipal());
+
+        assertEquals(MY_PRINCIPAL, c.oneByType(Object.class));
+        assertEquals(MY_PRINCIPAL, c.oneByType(String.class));
+        assertNull(c.oneByType(Integer.class));
+
+        assertEquals(Set.of(MY_PRINCIPAL, OTHER_PRINCIPAL, THIRD_PRINCIPAL), 
c.byType(Object.class));
+        assertEquals(Set.of(MY_PRINCIPAL, OTHER_PRINCIPAL, THIRD_PRINCIPAL), 
c.byType(String.class));
+        assertEquals(Set.of(), c.byType(Integer.class));
+
+        assertEquals(Set.of(MY_PRINCIPAL, OTHER_PRINCIPAL, THIRD_PRINCIPAL), 
c.asSet());
+
+        // principals are returned sorted by realm, then within each realm by 
insertion order
+        assertEquals(List.of(MY_PRINCIPAL, THIRD_PRINCIPAL, OTHER_PRINCIPAL), 
c.asList());
+
+        assertEquals(Set.of(MY_PRINCIPAL, THIRD_PRINCIPAL), 
c.fromRealm(MY_REALM));
+        assertEquals(Set.of(OTHER_PRINCIPAL), c.fromRealm(OTHER_REALM));
+
+        assertEquals(Set.of(MY_REALM, OTHER_REALM), c.getRealmNames());
+        assertFalse(c.isEmpty());
+    }
+
+}

Reply via email to