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