001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.subject;
020
021import org.apache.shiro.util.CollectionUtils;
022import org.apache.shiro.lang.util.StringUtils;
023
024import java.io.IOException;
025import java.io.ObjectInputStream;
026import java.io.ObjectOutputStream;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.Collections;
030import java.util.Iterator;
031import java.util.LinkedHashMap;
032import java.util.LinkedHashSet;
033import java.util.List;
034import java.util.Map;
035import java.util.Objects;
036import java.util.Set;
037
038/**
039 * A simple implementation of the {@link MutablePrincipalCollection} interface that tracks principals internally
040 * by storing them in a {@link LinkedHashMap}.
041 *
042 * @since 0.9
043 */
044@SuppressWarnings({"unchecked"})
045public class SimplePrincipalCollection implements MutablePrincipalCollection {
046
047    // Serialization reminder:
048    // You _MUST_ change this number if you introduce a change to this class
049    // that is NOT serialization backwards compatible.  Serialization-compatible
050    // changes do not require a change to this number.  If you need to generate
051    // a new number in this case, use the JDK's 'serialver' program to generate it.
052    private static final long serialVersionUID = -6305224034025797558L;
053
054    //TODO - complete JavaDoc
055    private Map<String, Set> realmPrincipals;
056
057    //cached toString() result, as this can be printed many times in logging
058    private transient String cachedToString;
059
060    public SimplePrincipalCollection() {
061    }
062
063    public SimplePrincipalCollection(Object principal, String realmName) {
064        if (principal instanceof Collection) {
065            addAll((Collection) principal, realmName);
066        } else {
067            add(principal, realmName);
068        }
069    }
070
071    public SimplePrincipalCollection(Collection principals, String realmName) {
072        addAll(principals, realmName);
073    }
074
075    public SimplePrincipalCollection(PrincipalCollection principals) {
076        addAll(principals);
077    }
078
079    protected Collection getPrincipalsLazy(String realmName) {
080        if (realmPrincipals == null) {
081            realmPrincipals = new LinkedHashMap<String, Set>();
082        }
083        Set principals = realmPrincipals.get(realmName);
084        if (principals == null) {
085            principals = new LinkedHashSet();
086            realmPrincipals.put(realmName, principals);
087        }
088        return principals;
089    }
090
091    /**
092     * Returns the first available principal from any of the {@code Realm} principals, or {@code null} if there are
093     * no principals yet.
094     * <p/>
095     * The 'first available principal' is interpreted as the principal that would be returned by
096     * <code>{@link #iterator() iterator()}.{@link java.util.Iterator#next() next()}.</code>
097     *
098     * @inheritDoc
099     */
100    public Object getPrimaryPrincipal() {
101        if (isEmpty()) {
102            return null;
103        }
104        return iterator().next();
105    }
106
107    public void add(Object principal, String realmName) {
108        if (realmName == null) {
109            throw new NullPointerException("realmName argument cannot be null.");
110        }
111        if (principal == null) {
112            throw new NullPointerException("principal argument cannot be null.");
113        }
114        this.cachedToString = null;
115        getPrincipalsLazy(realmName).add(principal);
116    }
117
118    public void addAll(Collection principals, String realmName) {
119        if (realmName == null) {
120            throw new NullPointerException("realmName argument cannot be null.");
121        }
122        if (principals == null) {
123            throw new NullPointerException("principals argument cannot be null.");
124        }
125        if (principals.isEmpty()) {
126            throw new IllegalArgumentException("principals argument cannot be an empty collection.");
127        }
128        this.cachedToString = null;
129        getPrincipalsLazy(realmName).addAll(principals);
130    }
131
132    public void addAll(PrincipalCollection principals) {
133        if (principals.getRealmNames() != null) {
134            for (String realmName : principals.getRealmNames()) {
135                for (Object principal : principals.fromRealm(realmName)) {
136                    add(principal, realmName);
137                }
138            }
139        }
140    }
141
142    public <T> T oneByType(Class<T> type) {
143        if (realmPrincipals == null || realmPrincipals.isEmpty()) {
144            return null;
145        }
146        Collection<Set> values = realmPrincipals.values();
147        for (Set set : values) {
148            for (Object o : set) {
149                if (type.isAssignableFrom(o.getClass())) {
150                    return (T) o;
151                }
152            }
153        }
154        return null;
155    }
156
157    public <T> Collection<T> byType(Class<T> type) {
158        if (realmPrincipals == null || realmPrincipals.isEmpty()) {
159            return Collections.EMPTY_SET;
160        }
161        Set<T> typed = new LinkedHashSet<T>();
162        Collection<Set> values = realmPrincipals.values();
163        for (Set set : values) {
164            for (Object o : set) {
165                if (type.isAssignableFrom(o.getClass())) {
166                    typed.add((T) o);
167                }
168            }
169        }
170        if (typed.isEmpty()) {
171            return Collections.EMPTY_SET;
172        }
173        return Collections.unmodifiableSet(typed);
174    }
175
176    public List asList() {
177        Set all = asSet();
178        if (all.isEmpty()) {
179            return Collections.EMPTY_LIST;
180        }
181        return Collections.unmodifiableList(new ArrayList(all));
182    }
183
184    public Set asSet() {
185        if (realmPrincipals == null || realmPrincipals.isEmpty()) {
186            return Collections.EMPTY_SET;
187        }
188        Set aggregated = new LinkedHashSet();
189        Collection<Set> values = realmPrincipals.values();
190        for (Set set : values) {
191            aggregated.addAll(set);
192        }
193        if (aggregated.isEmpty()) {
194            return Collections.EMPTY_SET;
195        }
196        return Collections.unmodifiableSet(aggregated);
197    }
198
199    public Collection fromRealm(String realmName) {
200        if (realmPrincipals == null || realmPrincipals.isEmpty()) {
201            return Collections.EMPTY_SET;
202        }
203        Set principals = realmPrincipals.get(realmName);
204        if (principals == null || principals.isEmpty()) {
205            principals = Collections.EMPTY_SET;
206        }
207        return Collections.unmodifiableSet(principals);
208    }
209
210    public Set<String> getRealmNames() {
211        if (realmPrincipals == null) {
212            return null;
213        } else {
214            return realmPrincipals.keySet();
215        }
216    }
217
218    public boolean isEmpty() {
219        return realmPrincipals == null || realmPrincipals.isEmpty();
220    }
221
222    public void clear() {
223        this.cachedToString = null;
224        if (realmPrincipals != null) {
225            realmPrincipals.clear();
226            realmPrincipals = null;
227        }
228    }
229
230    public Iterator iterator() {
231        return asSet().iterator();
232    }
233
234    public boolean equals(Object o) {
235        if (o == this) {
236            return true;
237        }
238        if (o instanceof SimplePrincipalCollection) {
239            SimplePrincipalCollection other = (SimplePrincipalCollection) o;
240            return Objects.equals(this.realmPrincipals, other.realmPrincipals);
241        }
242        return false;
243    }
244
245    public int hashCode() {
246        if (this.realmPrincipals != null && !realmPrincipals.isEmpty()) {
247            return realmPrincipals.hashCode();
248        }
249        return super.hashCode();
250    }
251
252    /**
253     * Returns a simple string representation suitable for printing.
254     *
255     * @return a simple string representation suitable for printing.
256     * @since 1.0
257     */
258    public String toString() {
259        if (this.cachedToString == null) {
260            Set<Object> principals = asSet();
261            if (!CollectionUtils.isEmpty(principals)) {
262                this.cachedToString = StringUtils.toString(principals.toArray());
263            } else {
264                this.cachedToString = "empty";
265            }
266        }
267        return this.cachedToString;
268    }
269
270
271    /**
272     * Serialization write support.
273     * <p/>
274     * NOTE: Don't forget to change the serialVersionUID constant at the top of this class
275     * if you make any backwards-incompatible serialization changes!!!
276     * (use the JDK 'serialver' program for this)
277     *
278     * @param out output stream provided by Java serialization
279     * @throws IOException if there is a stream error
280     */
281    private void writeObject(ObjectOutputStream out) throws IOException {
282        out.defaultWriteObject();
283        boolean principalsExist = !CollectionUtils.isEmpty(realmPrincipals);
284        out.writeBoolean(principalsExist);
285        if (principalsExist) {
286            out.writeObject(realmPrincipals);
287        }
288    }
289
290    /**
291     * Serialization read support - reads in the Map principals collection if it exists in the
292     * input stream.
293     * <p/>
294     * NOTE: Don't forget to change the serialVersionUID constant at the top of this class
295     * if you make any backwards-incompatible serialization changes!!!
296     * (use the JDK 'serialver' program for this)
297     *
298     * @param in input stream provided by
299     * @throws IOException            if there is an input/output problem
300     * @throws ClassNotFoundException if the underlying Map implementation class is not available to the classloader.
301     */
302    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
303        in.defaultReadObject();
304        boolean principalsExist = in.readBoolean();
305        if (principalsExist) {
306            this.realmPrincipals = (Map<String, Set>) in.readObject();
307        }
308    }
309}