001: /* Copyright 2001, 2002 The JA-SIG Collaborative. All rights reserved.
002: * See license distributed with this file and
003: * available online at http://www.uportal.org/license.html
004: */
005:
006: package org.jasig.portal.groups;
007:
008: import java.util.ArrayList;
009: import java.util.Collection;
010: import java.util.HashMap;
011: import java.util.HashSet;
012: import java.util.Iterator;
013: import java.util.Set;
014:
015: import javax.naming.Name;
016:
017: import org.jasig.portal.EntityIdentifier;
018: import org.jasig.portal.services.GroupService;
019:
020: /**
021: * Reference implementation for <code>IEntityGroup</code>.
022: * <p>
023: * Groups do not keep references to their members but instead cache
024: * member keys. The members are cached externally. The rules
025: * for controlling access to the key caches are a bit obscure, but you
026: * should understand them before writing code that updates groups.
027: * Access to the caches themselves is synchronized via the cache
028: * getters and setters. All requests to get group members and to add or
029: * remove group members ultimately go through these methods. The
030: * mutating methods, <code>addMember()</code> and <code>removeMember()</code>
031: * however, do a copy-on-write. That is, they first make a copy of the
032: * cache, add or remove the member key, and then replace the original
033: * cache with the copy. This permits multiple read and write threads to run
034: * concurrently without throwing <code>ConcurrentModificationExceptions</code>.
035: * But it still leaves open the danger of data races because nothing in
036: * this class guarantees serialized write access. You must impose this
037: * from without, either via explicit locking (<code>GroupService.getLockableGroup()</code>)
038: * or by synchronizing access from the caller.
039: *
040: * @author Dan Ellentuck
041: * @version $Revision: 36518 $
042: * @see org.jasig.portal.groups.IEntityGroup
043: *
044: *
045: */
046: public class EntityGroupImpl extends GroupMemberImpl implements
047: IEntityGroup {
048: private String creatorID;
049: private String name;
050: private String description;
051: protected IIndividualGroupService localGroupService;
052:
053: // A group and its members share an entityType.
054: private java.lang.Class leafEntityType;
055:
056: /*
057: * The Sets of keys to the members of this group. The <code>IGroupMembers</code>
058: * themselves are cached by the service.
059: */
060: private Set memberGroupKeys;
061: private Set memberEntityKeys;
062: private boolean memberKeysInitialized;
063:
064: /*
065: * References to updated group members. These updates do not become visible to
066: * the members until the update is committed.
067: */
068: private HashMap addedMembers;
069: private HashMap removedMembers;
070:
071: /**
072: * EntityGroupImpl
073: */
074: public EntityGroupImpl(String groupKey, Class entityType)
075: throws GroupsException {
076: super (new CompositeEntityIdentifier(groupKey,
077: org.jasig.portal.EntityTypes.GROUP_ENTITY_TYPE));
078: if (isKnownEntityType(entityType)) {
079: leafEntityType = entityType;
080: } else {
081: throw new GroupsException("Unknown entity type: "
082: + entityType);
083: }
084: }
085:
086: /**
087: * Adds <code>IGroupMember</code> gm to our member <code>Map</code> and conversely,
088: * adds <code>this</code> to gm's group <code>Map</code>, after checking that the
089: * addition does not violate group rules. Remember that we have added it so we can
090: * update the database if necessary.
091: * @param gm org.jasig.portal.groups.IGroupMember
092: */
093: public void addMember(IGroupMember gm) throws GroupsException {
094: try {
095: checkProspectiveMember(gm);
096: } catch (GroupsException ge) {
097: throw new GroupsException("Could not add IGroupMember", ge);
098: }
099:
100: if (!this .contains(gm)) {
101: Object cacheKey = gm.getEntityIdentifier().getKey();
102:
103: if (getRemovedMembers().containsKey(cacheKey)) {
104: getRemovedMembers().remove(cacheKey);
105: } else {
106: getAddedMembers().put(cacheKey, gm);
107: }
108:
109: if (memberKeysInitialized) {
110: primAddMember(gm);
111: }
112: }
113: }
114:
115: /**
116: * @return boolean
117: */
118: protected boolean areMemberKeysInitialized() {
119: return memberKeysInitialized;
120: }
121:
122: /**
123: * A member must share the <code>entityType</code> of its containing <code>IEntityGroup</code>.
124: * If it is a group, it must have a unique name within each of its containing groups and
125: * the resulting group must not contain a circular reference.
126: * Removed the requirement for unique group names. (03-04-2004, de)
127: * @param gm org.jasig.portal.groups.IGroupMember
128: * @exception org.jasig.portal.groups.GroupsException
129: */
130: private void checkProspectiveMember(IGroupMember gm)
131: throws GroupsException {
132: if (gm.equals(this )) {
133: throw new GroupsException("Attempt to add " + gm
134: + " to itself.");
135: }
136:
137: // Type check:
138: if (this .getLeafType() != gm.getLeafType()) {
139: throw new GroupsException(this + " and " + gm
140: + " have different entity types.");
141: }
142:
143: // Circular reference check:
144: if (gm.isGroup() && gm.deepContains(this )) {
145: throw new GroupsException("Adding " + gm + " to " + this
146: + " creates a circular reference.");
147: }
148: }
149:
150: /**
151: * Clear out caches for pending adds and deletes of group members.
152: */
153: protected void clearPendingUpdates() {
154: addedMembers = null;
155: removedMembers = null;
156: }
157:
158: /**
159: * Clone the member entity keys.
160: * @return Set
161: */
162: private Set copyMemberEntityKeys() throws GroupsException {
163: return castAndCopyHashSet(getMemberEntityKeys());
164: }
165:
166: /**
167: * Clone the member group keys.
168: * @return Set
169: */
170: private Set copyMemberGroupKeys() throws GroupsException {
171: return castAndCopyHashSet(getMemberGroupKeys());
172: }
173:
174: /**
175: * Checks if <code>GroupMember</code> gm is a member of this.
176: * @return boolean
177: * @param gm org.jasig.portal.groups.IGroupMember
178: */
179: public boolean contains(IGroupMember gm) throws GroupsException {
180: if (areMemberKeysInitialized()) {
181: Object cacheKey = gm.getKey();
182: return getMemberGroupKeys().contains(cacheKey)
183: || getMemberEntityKeys().contains(cacheKey);
184: } else {
185: return gm.isMemberOf(this );
186: }
187: }
188:
189: /**
190: * Checks recursively if <code>GroupMember</code> gm is a member of this.
191: * @return boolean
192: * @param gm org.jasig.portal.groups.IGroupMember
193: */
194: public boolean deepContains(IGroupMember gm) throws GroupsException {
195: if (this .contains(gm)) {
196: return true;
197: }
198:
199: boolean found = false;
200: Iterator it = getMemberGroups();
201: while (it.hasNext() && !found) {
202: IEntityGroup group = (IEntityGroup) it.next();
203: found = group.deepContains(gm);
204: }
205:
206: return found;
207: }
208:
209: /**
210: * Delegates to the factory.
211: */
212: public void delete() throws GroupsException {
213: getLocalGroupService().deleteGroup(this );
214: }
215:
216: /**
217: * @param obj the Object to compare with
218: * @return true if these Objects are equal; false otherwise.
219: * @see java.util.Hashtable
220: */
221: public boolean equals(Object obj) {
222: if (obj == null)
223: return false;
224: if (obj == this )
225: return true;
226: if (!(obj instanceof EntityGroupImpl))
227: return false;
228:
229: return this .getKey().equals(((IGroupMember) obj).getKey());
230: }
231:
232: /**
233: * @return java.util.HashMap
234: */
235: public HashMap getAddedMembers() {
236: if (this .addedMembers == null)
237: this .addedMembers = new HashMap();
238: return addedMembers;
239: }
240:
241: /**
242: * Returns an <code>Iterator</code> over the <code>Set</code> of this
243: * <code>IEntityGroup's</code> recursively-retrieved members that are
244: * <code>IEntities</code>.
245: * @return java.util.Iterator
246: */
247: public java.util.Iterator getAllEntities() throws GroupsException {
248: return primGetAllEntities(new HashSet()).iterator();
249: }
250:
251: /**
252: * Returns an <code>Iterator</code> over the <code>Set</code> of recursively-retrieved
253: * <code>IGroupMembers</code> that are members of this <code>IEntityGroup</code>.
254: * @return java.util.Iterator
255: */
256: public java.util.Iterator getAllMembers() throws GroupsException {
257: return primGetAllMembers(new HashSet()).iterator();
258: }
259:
260: /**
261: * Returns the <code>EntityIdentifier</code> cast to a
262: * <code>CompositeEntityIdentifier</code> so that its service nodes
263: * can be pushed and popped.
264: *
265: * @return CompositeEntityIdentifier
266: */
267: protected CompositeEntityIdentifier getCompositeEntityIdentifier() {
268: return (CompositeEntityIdentifier) getEntityIdentifier();
269: }
270:
271: /**
272: * @return java.lang.String
273: */
274: public java.lang.String getCreatorID() {
275: return creatorID;
276: }
277:
278: /**
279: * @return java.lang.String
280: */
281: public java.lang.String getDescription() {
282: return description;
283: }
284:
285: /**
286: * Returns an <code>Iterator</code> over this <code>IEntityGroup's</code>
287: * members that are <code>IEntities</code>.
288: * @return java.util.Iterator
289: */
290: public java.util.Iterator getEntities() throws GroupsException {
291: return getMemberEntities();
292: }
293:
294: /**
295: * @return EntityIdentifier
296: */
297: public EntityIdentifier getEntityIdentifier() {
298: return getUnderlyingEntityIdentifier();
299: }
300:
301: /**
302: * Returns the key of the underyling entity.
303: * @return java.lang.String
304: */
305: public String getEntityKey() {
306: return getKey();
307: }
308:
309: /**
310: * Returns the entity type of this groups's leaf members.
311: *
312: * @return java.lang.Class
313: * @see org.jasig.portal.EntityTypes
314: */
315: public java.lang.Class getEntityType() {
316: return leafEntityType;
317: }
318:
319: /**
320: * @return String
321: */
322: public String getGroupID() {
323: return getKey();
324: }
325:
326: /**
327: * Returns the entity type of this groups's members.
328: *
329: * @return java.lang.Class
330: * @see org.jasig.portal.EntityTypes
331: */
332: public java.lang.Class getLeafType() {
333: return leafEntityType;
334: }
335:
336: /**
337: * @return IIndividualGroupService
338: */
339: protected IIndividualGroupService getLocalGroupService() {
340: return localGroupService;
341: }
342:
343: /**
344: * Returns the key from the group service of origin.
345: * @return String
346: */
347: public String getLocalKey() {
348: return getCompositeEntityIdentifier().getLocalKey();
349: }
350:
351: /**
352: * Returns an <code>Iterator</code> over the entities in our member
353: * <code>Collection</code>.
354: * @return java.util.Iterator
355: */
356: protected java.util.Iterator getMemberEntities()
357: throws GroupsException {
358: Collection members = new ArrayList();
359: for (Iterator i = getMemberEntityKeys().iterator(); i.hasNext();) {
360: String key = (String) i.next();
361: members.add(getLocalGroupService().getEntity(key,
362: getLeafType()));
363: }
364: return members.iterator();
365: }
366:
367: /**
368: * @return java.util.Set
369: */
370: private synchronized Set getMemberEntityKeys()
371: throws GroupsException {
372: if (!areMemberKeysInitialized()) {
373: initializeMembers();
374: }
375: return memberEntityKeys;
376: }
377:
378: /**
379: * @return java.util.Set
380: */
381: private synchronized Set getMemberGroupKeys()
382: throws GroupsException {
383: if (!areMemberKeysInitialized()) {
384: initializeMembers();
385: }
386: return memberGroupKeys;
387: }
388:
389: /**
390: * Returns the named member <code>IEntityGroup</code>.
391: * @return org.jasig.portal.groups.IEntityGroup
392: * @param name java.lang.String
393: */
394: public IEntityGroup getMemberGroupNamed(String name)
395: throws GroupsException {
396: IGroupMember gm;
397: for (Iterator itr = getMemberGroups(); itr.hasNext();) {
398: gm = (IGroupMember) itr.next();
399: if (((IEntityGroup) gm).getName().equals(name)) {
400: return (IEntityGroup) gm;
401: }
402: }
403: return null;
404: }
405:
406: /**
407: * Returns an <code>Iterator</code> over the groups in our member
408: * <code>Collection</code>.
409: * @return java.util.Iterator
410: */
411: protected java.util.Iterator getMemberGroups()
412: throws GroupsException {
413: Collection members = new ArrayList();
414: for (Iterator i = getMemberGroupKeys().iterator(); i.hasNext();) {
415: String key = (String) i.next();
416: members.add(GroupService.findGroup(key));
417: }
418: return members.iterator();
419: }
420:
421: /**
422: * Returns an <code>Iterator</code> over the <code>GroupMembers</code> in our
423: * member <code>Collection</code>.
424: * @return java.util.Iterator
425: */
426: public java.util.Iterator getMembers() throws GroupsException {
427: Collection members = new ArrayList(100);
428: Iterator itr = null;
429:
430: for (itr = getMemberGroups(); itr.hasNext();) {
431: members.add(itr.next());
432: }
433: for (itr = getMemberEntities(); itr.hasNext();) {
434: members.add(itr.next());
435: }
436:
437: return members.iterator();
438: }
439:
440: /**
441: * @return java.lang.String
442: */
443: public java.lang.String getName() {
444: return name;
445: }
446:
447: /**
448: * @return java.util.HashMap
449: */
450: public HashMap getRemovedMembers() {
451: if (this .removedMembers == null)
452: this .removedMembers = new HashMap();
453: return removedMembers;
454: }
455:
456: /**
457: * @return IGroupService
458: */
459: protected GroupService getService() throws GroupsException {
460: return GroupService.instance();
461: }
462:
463: /**
464: * Returns the Name of the group service of origin.
465: * @return javax.naming.Nme
466: */
467: public Name getServiceName() {
468: return getCompositeEntityIdentifier().getServiceName();
469: }
470:
471: /**
472: * Returns this object's type for purposes of caching and locking, as
473: * opposed to the underlying entity type.
474: *
475: * @return java.lang.Class
476: */
477: public Class getType() {
478: return org.jasig.portal.EntityTypes.GROUP_ENTITY_TYPE;
479: }
480:
481: /**
482: * Answers if there are any added memberships not yet committed to the database.
483: * @return boolean
484: */
485: public boolean hasAdds() {
486: return (addedMembers != null) && (addedMembers.size() > 0);
487: }
488:
489: /**
490: * Answers if there are any deleted memberships not yet committed to the database.
491: * @return boolean
492: */
493: public boolean hasDeletes() {
494: return (removedMembers != null) && (removedMembers.size() > 0);
495: }
496:
497: /**
498: * Generates a hash code for the receiver.
499: * This method is supported primarily for
500: * hash tables, such as those provided in java.util.
501: * @return an integer hash code for the receiver
502: * @see java.util.Hashtable
503: */
504: public int hashCode() {
505: return getKey().hashCode();
506: }
507:
508: /**
509: * @return boolean
510: */
511: public boolean hasMembers() throws GroupsException {
512: return getMembers().hasNext();
513: }
514:
515: /**
516: * Cache the <code>IEntityGroup</code> members.
517: */
518: private void initializeMembers() throws GroupsException {
519: Set groupKeys = new HashSet();
520: Set entityKeys = new HashSet(100);
521:
522: for (Iterator it = getLocalGroupService().findMembers(this ); it
523: .hasNext();) {
524: IGroupMember gm = (IGroupMember) it.next();
525: Set cache = (gm.isGroup()) ? groupKeys : entityKeys;
526: cache.add(gm.getKey());
527: }
528: setMemberEntityKeys(entityKeys);
529: setMemberGroupKeys(groupKeys);
530: setMemberKeysInitialized(true);
531: }
532:
533: /**
534: * Answers if there are any added or deleted memberships not yet committed to the database.
535: * @return boolean
536: */
537: public boolean isDirty() {
538: return hasAdds() || hasDeletes();
539: }
540:
541: /**
542: * Answers if this <code>IEntityGroup</code> can be changed or deleted.
543: * @return boolean
544: * @exception GroupsException
545: */
546: public boolean isEditable() throws GroupsException {
547: return getLocalGroupService().isEditable(this );
548: }
549:
550: /**
551: * @return boolean
552: */
553: public boolean isGroup() {
554: return true;
555: }
556:
557: /**
558: * Adds the <code>IGroupMember</code> key to the appropriate member key
559: * cache by copying the cache, adding to the copy, and then replacing the
560: * original with the copy. At this point, <code>gm</code> does not yet
561: * have <code>this</code> in its containing group cache. That cache entry
562: * is not added until update(), when changes are committed to the store.
563: * @param gm org.jasig.portal.groups.IGroupMember
564: */
565: protected void primAddMember(IGroupMember gm)
566: throws GroupsException {
567: Set cache = (gm.isGroup()) ? copyMemberGroupKeys()
568: : copyMemberEntityKeys();
569: cache.add(gm.getKey());
570: if (gm.isGroup())
571: setMemberGroupKeys(cache);
572: else
573: setMemberEntityKeys(cache);
574: }
575:
576: /**
577: * Returns the <code>Set</code> of <code>IEntities</code> in our member <code>Collection</code>
578: * and, recursively, in the <code>Collections</code> of our members.
579: * @param entities a Set that IEntity-GroupMembers are added to.
580: * @return java.util.Set
581: */
582: protected java.util.Set primGetAllEntities(Set entities)
583: throws GroupsException {
584: Iterator i = getMembers();
585: while (i.hasNext()) {
586: GroupMemberImpl gmi = (GroupMemberImpl) i.next();
587: if (gmi.isEntity()) {
588: entities.add(gmi);
589: } else {
590: ((EntityGroupImpl) gmi).primGetAllEntities(entities);
591: }
592: }
593: return entities;
594: }
595:
596: /**
597: * Returns the <code>Set</code> of <code>IGroupMembers</code> in our member
598: * <code>Collection</code> and, recursively, in the <code>Collections</code>
599: * of our members.
600: * @param s java.lang.Set - a Set that members are added to.
601: * @return java.util.Set
602: */
603: protected java.util.Set primGetAllMembers(Set s)
604: throws GroupsException {
605: Iterator i = getMembers();
606: while (i.hasNext()) {
607: GroupMemberImpl gmi = (GroupMemberImpl) i.next();
608: s.add(gmi);
609: if (gmi.isGroup()) {
610: ((EntityGroupImpl) gmi).primGetAllMembers(s);
611: }
612: }
613: return s;
614: }
615:
616: /**
617: * Removes the <code>IGroupMember</code> key from the appropriate key cache, by
618: * copying the cache, removing the key from the copy and replacing the original
619: * with the copy. At this point, <code>gm</code> still has <code>this</code>
620: * in its containing groups cache. That cache entry is not removed until update(),
621: * when changes are committed to the store.
622: * @param gm org.jasig.portal.groups.IGroupMember
623: */
624: protected void primRemoveMember(IGroupMember gm)
625: throws GroupsException {
626: Set cache = (gm.isGroup()) ? copyMemberGroupKeys()
627: : copyMemberEntityKeys();
628: cache.remove(gm.getKey());
629: if (gm.isGroup())
630: setMemberGroupKeys(cache);
631: else
632: setMemberEntityKeys(cache);
633: }
634:
635: /**
636: * @param newName java.lang.String
637: */
638: public void primSetName(java.lang.String newName) {
639: name = newName;
640: }
641:
642: /**
643: * Removes <code>IGroupMember</code> gm from our member <code>Map</code> and,
644: * conversely, remove this from gm's group <code>Map</code>. Remember that we
645: * have removed it so we can update the database, if necessary.
646: * @param gm org.jasig.portal.groups.IGroupMember
647: */
648: public void removeMember(IGroupMember gm) throws GroupsException {
649: Object cacheKey = gm.getEntityIdentifier().getKey();
650:
651: if (getAddedMembers().containsKey(cacheKey)) {
652: getAddedMembers().remove(cacheKey);
653: } else {
654: getRemovedMembers().put(cacheKey, gm);
655: }
656:
657: primRemoveMember(gm);
658: }
659:
660: /**
661: * @param newCreatorID java.lang.String
662: */
663: public void setCreatorID(java.lang.String newCreatorID) {
664: creatorID = newCreatorID;
665: }
666:
667: /**
668: * @param newDescription java.lang.String
669: */
670: public void setDescription(java.lang.String newDescription) {
671: description = newDescription;
672: }
673:
674: /**
675: * @param newIndividualGroupService IIndividualGroupService
676: */
677: public void setLocalGroupService(
678: IIndividualGroupService newIndividualGroupService)
679: throws GroupsException {
680: localGroupService = newIndividualGroupService;
681: setServiceName(localGroupService.getServiceName());
682: }
683:
684: /**
685: * @param newMemberKeysInitialized boolean
686: */
687: private void setMemberKeysInitialized(
688: boolean newMemberKeysInitialized) {
689: memberKeysInitialized = newMemberKeysInitialized;
690: }
691:
692: /**
693: * @param newMemberEntityKeys Set
694: */
695: private synchronized void setMemberEntityKeys(
696: Set newMemberEntityKeys) {
697: memberEntityKeys = newMemberEntityKeys;
698: }
699:
700: /**
701: * @param newMemberGroupKeys Set
702: */
703: private synchronized void setMemberGroupKeys(Set newMemberGroupKeys) {
704: memberGroupKeys = newMemberGroupKeys;
705: }
706:
707: /**
708: * We used to check duplicate sibling names but no longer do.
709: * @param newName java.lang.String
710: */
711: public void setName(java.lang.String newName)
712: throws GroupsException {
713: primSetName(newName);
714: }
715:
716: /**
717: * Sets the service Name of the group service of origin.
718: */
719: public void setServiceName(Name newServiceName)
720: throws GroupsException {
721: try {
722: getCompositeEntityIdentifier().setServiceName(
723: newServiceName);
724: } catch (javax.naming.InvalidNameException ine) {
725: throw new GroupsException("Problem setting service name",
726: ine);
727: }
728:
729: }
730:
731: /**
732: * Returns a String that represents the value of this object.
733: * @return a string representation of the receiver
734: */
735: public String toString() {
736: return "EntityGroupImpl (" + getKey() + ") " + getName();
737: }
738:
739: /**
740: * Delegate to the factory.
741: */
742: public void update() throws GroupsException {
743: getLocalGroupService().updateGroup(this );
744: clearPendingUpdates();
745: }
746:
747: /**
748: * Delegate to the factory.
749: */
750: public void updateMembers() throws GroupsException {
751: getLocalGroupService().updateGroupMembers(this);
752: clearPendingUpdates();
753: }
754: }
|