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