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()); + } + +}
