DynamicGroupMemberList.java revision ea1068c292e9b341af6d6b563cd8988a96be20a9
/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2008 Sun Microsystems, Inc.
* Portions Copyright 2014-2015 ForgeRock AS
*/
package org.opends.server.extensions;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.LDAPURL;
import org.opends.server.types.MemberList;
import org.opends.server.types.MembershipException;
import org.opends.server.types.SearchFilter;
import org.forgerock.opendj.ldap.SearchScope;
/**
* This class defines a mechanism that may be used to iterate over the
* members of a dynamic group, optionally using an additional set of
* criteria to further filter the results.
*/
public class DynamicGroupMemberList
extends MemberList
{
// Indicates whether the search thread has completed its processing.
private boolean searchesCompleted;
// The base DN to use when filtering the set of group members.
private final DN baseDN;
// The DN of the entry containing the group definition.
private final DN groupDN;
// The queue into which results will be placed while they are waiting to be
// returned. The types of objects that may be placed in this queue are Entry
// objects to return or MembershipException objects to throw.
private final LinkedBlockingQueue<Object> resultQueue;
// The search filter to use when filtering the set of group members.
private final SearchFilter filter;
// The search scope to use when filtering the set of group members.
private final SearchScope scope;
// The set of LDAP URLs that define the membership criteria.
private final Set<LDAPURL> memberURLs;
/**
* Creates a new dynamic group member list with the provided information.
*
* @param groupDN The DN of the entry containing the group definition.
* @param memberURLs The set of LDAP URLs that define the membership
* criteria for the associated group.
*
* @throws DirectoryException If a problem occurs while creating the member
* list.
*/
public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs)
throws DirectoryException
{
this(groupDN, memberURLs, null, null, null);
}
/**
* Creates a new dynamic group member list with the provided information.
*
* @param groupDN The DN of the entry containing the group definition.
* @param memberURLs The set of LDAP URLs that define the membership
* criteria for the associated group.
* @param baseDN The base DN that should be enforced for all entries to
* return.
* @param scope The scope that should be enforced for all entries to
* return.
* @param filter The filter that should be enforced for all entries to
* return.
*
* @throws DirectoryException If a problem occurs while creating the member
* list.
*/
public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs,
DN baseDN, SearchScope scope,
SearchFilter filter)
throws DirectoryException
{
this.groupDN = groupDN;
this.memberURLs = memberURLs;
this.baseDN = baseDN;
this.filter = filter;
if (scope == null)
{
this.scope = SearchScope.WHOLE_SUBTREE;
}
else
{
this.scope = scope;
}
searchesCompleted = false;
resultQueue = new LinkedBlockingQueue<Object>(10);
// We're going to have to perform one or more internal searches in order to
// get the results. We need to be careful about the way that we construct
// them in order to avoid the possibility of getting duplicate results, so
// searches with overlapping bases will need to be combined.
LinkedHashMap<DN,LinkedList<LDAPURL>> baseDNs =
new LinkedHashMap<DN,LinkedList<LDAPURL>>();
for (LDAPURL memberURL : memberURLs)
{
// First, determine the base DN for the search. It needs to be evaluated
// as relative to both the overall base DN specified in the set of
// criteria, as well as any other existing base DNs in the same hierarchy.
DN urlBaseDN = memberURL.getBaseDN();
if (baseDN != null)
{
if (baseDN.isDescendantOf(urlBaseDN))
{
// The base DN requested by the user is below the base DN for this
// URL, so we'll use the base DN requested by the user.
urlBaseDN = baseDN;
}
else if (! urlBaseDN.isDescendantOf(baseDN))
{
// The base DN from the URL is outside the base requested by the user,
// so we can skip this URL altogether.
continue;
}
}
// If this is the first URL, then we can just add it with the base DN.
// Otherwise, we need to see if it needs to be merged with other URLs in
// the same hierarchy.
if (baseDNs.isEmpty())
{
LinkedList<LDAPURL> urlList = new LinkedList<LDAPURL>();
urlList.add(memberURL);
baseDNs.put(urlBaseDN, urlList);
}
else
{
// See if the specified base DN is already in the map. If so, then
// just add the new URL to the existing list.
LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN);
if (urlList == null)
{
// There's no existing list for the same base DN, but there might be
// DNs in an overlapping hierarchy. If so, then use the base DN that
// is closest to the naming context. If not, then add a new list with
// the current base DN.
boolean found = false;
Iterator<DN> iterator = baseDNs.keySet().iterator();
while (iterator.hasNext())
{
DN existingBaseDN = iterator.next();
if (urlBaseDN.isDescendantOf(existingBaseDN))
{
// The base DN for the current URL is below an existing base DN,
// so we can just add this URL to the existing list and be done.
urlList = baseDNs.get(existingBaseDN);
urlList.add(memberURL);
found = true;
break;
}
else if (existingBaseDN.isDescendantOf(urlBaseDN))
{
// The base DN for the current URL is above the existing base DN,
// so we should use the base DN for the current URL instead of the
// existing one.
urlList = baseDNs.get(existingBaseDN);
urlList.add(memberURL);
iterator.remove();
baseDNs.put(urlBaseDN, urlList);
found = true;
break;
}
}
if (! found)
{
urlList = new LinkedList<LDAPURL>();
urlList.add(memberURL);
baseDNs.put(urlBaseDN, urlList);
}
}
else
{
// There was already a list with the same base DN, so just add the
// URL.
urlList.add(memberURL);
}
}
}
// At this point, we should know what base DN(s) we need to use, so we can
// create the filter to use with that base DN. There are some special-case
// optimizations that we can do here, but in general the filter will look
// like "(&(filter)(|(urlFilters)))".
LinkedHashMap<DN,SearchFilter> searchMap =
new LinkedHashMap<DN,SearchFilter>();
for (DN urlBaseDN : baseDNs.keySet())
{
LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN);
LinkedHashSet<SearchFilter> urlFilters =
new LinkedHashSet<SearchFilter>();
for (LDAPURL url : urlList)
{
urlFilters.add(url.getFilter());
}
SearchFilter combinedFilter;
if (filter == null)
{
if (urlFilters.size() == 1)
{
combinedFilter = urlFilters.iterator().next();
}
else
{
combinedFilter = SearchFilter.createORFilter(urlFilters);
}
}
else
{
if (urlFilters.size() == 1)
{
SearchFilter urlFilter = urlFilters.iterator().next();
if (urlFilter.equals(filter))
{
combinedFilter = filter;
}
else
{
LinkedHashSet<SearchFilter> filterSet =
new LinkedHashSet<SearchFilter>();
filterSet.add(filter);
filterSet.add(urlFilter);
combinedFilter = SearchFilter.createANDFilter(filterSet);
}
}
else
{
if (urlFilters.contains(filter))
{
combinedFilter = filter;
}
else
{
LinkedHashSet<SearchFilter> filterSet =
new LinkedHashSet<SearchFilter>();
filterSet.add(filter);
filterSet.add(SearchFilter.createORFilter(urlFilters));
combinedFilter = SearchFilter.createANDFilter(filterSet);
}
}
}
searchMap.put(urlBaseDN, combinedFilter);
}
// At this point, we should have all the information we need to perform the
// searches. Create arrays of the elements for each.
DN[] baseDNArray = new DN[baseDNs.size()];
SearchFilter[] filterArray = new SearchFilter[baseDNArray.length];
LDAPURL[][] urlArray = new LDAPURL[baseDNArray.length][];
Iterator<DN> iterator = baseDNs.keySet().iterator();
for (int i=0; i < baseDNArray.length; i++)
{
baseDNArray[i] = iterator.next();
filterArray[i] = searchMap.get(baseDNArray[i]);
LinkedList<LDAPURL> urlList = baseDNs.get(baseDNArray[i]);
urlArray[i] = new LDAPURL[urlList.size()];
int j=0;
for (LDAPURL url : urlList)
{
urlArray[i][j++] = url;
}
}
DynamicGroupSearchThread searchThread =
new DynamicGroupSearchThread(this, baseDNArray, filterArray, urlArray);
searchThread.start();
}
/**
* Retrieves the DN of the dynamic group with which this dynamic group member
* list is associated.
*
* @return The DN of the dynamic group with which this dynamic group member
* list is associated.
*/
public final DN getDynamicGroupDN()
{
return groupDN;
}
/**
* Indicates that all of the searches needed to iterate across the member list
* have completed and there will not be any more results provided.
*/
final void setSearchesCompleted()
{
searchesCompleted = true;
}
/**
* Adds the provided entry to the set of results that should be returned for
* this member list.
*
* @param entry The entry to add to the set of results that should be
* returned for this member list.
*
* @return {@code true} if the entry was added to the result set, or
* {@code false} if it was not (either because a timeout expired or
* the attempt was interrupted). If this method returns
* {@code false}, then the search thread should terminate
* immediately.
*/
final boolean addResult(Entry entry)
{
try
{
return resultQueue.offer(entry, 10, TimeUnit.SECONDS);
}
catch (InterruptedException ie)
{
return false;
}
}
/**
* Adds the provided membership exception so that it will be thrown along with
* the set of results for this member list.
*
* @param membershipException The membership exception to be thrown.
*
* @return {@code true} if the exception was added to the result set, or
* {@code false} if it was not (either because a timeout expired or
* the attempt was interrupted). If this method returns
* {@code false}, then the search thread should terminate
* immediately.
*/
final boolean addResult(MembershipException membershipException)
{
try
{
return resultQueue.offer(membershipException, 10, TimeUnit.SECONDS);
}
catch (InterruptedException ie)
{
return false;
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasMoreMembers()
{
while (! searchesCompleted)
{
if (resultQueue.peek() != null)
{
return true;
}
try
{
Thread.sleep(0, 1000);
} catch (Exception e) {}
}
return (resultQueue.peek() != null);
}
/**
* {@inheritDoc}
*/
@Override
public Entry nextMemberEntry()
throws MembershipException
{
if (! hasMoreMembers())
{
return null;
}
Object result = resultQueue.poll();
if (result == null)
{
close();
return null;
}
else if (result instanceof Entry)
{
return (Entry) result;
}
else if (result instanceof MembershipException)
{
MembershipException me = (MembershipException) result;
if (! me.continueIterating())
{
close();
}
throw me;
}
// We should never get here.
close();
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void close()
{
searchesCompleted = true;
resultQueue.clear();
}
}