001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008 Sun Microsystems, Inc.
015 * Portions Copyright 2014-2016 ForgeRock AS.
016 */
017package org.opends.server.extensions;
018
019import static org.opends.server.util.CollectionUtils.*;
020
021import java.util.Iterator;
022import java.util.LinkedHashMap;
023import java.util.LinkedHashSet;
024import java.util.LinkedList;
025import java.util.Set;
026import java.util.concurrent.LinkedBlockingQueue;
027import java.util.concurrent.TimeUnit;
028
029import org.forgerock.opendj.ldap.SearchScope;
030import org.forgerock.opendj.ldap.DN;
031import org.opends.server.types.DirectoryException;
032import org.opends.server.types.Entry;
033import org.opends.server.types.LDAPURL;
034import org.opends.server.types.MemberList;
035import org.opends.server.types.MembershipException;
036import org.opends.server.types.SearchFilter;
037
038/**
039 * This class defines a mechanism that may be used to iterate over the
040 * members of a dynamic group, optionally using an additional set of
041 * criteria to further filter the results.
042 */
043public class DynamicGroupMemberList
044       extends MemberList
045{
046  /** Indicates whether the search thread has completed its processing. */
047  private boolean searchesCompleted;
048
049  /** The base DN to use when filtering the set of group members. */
050  private final DN baseDN;
051
052  /** The DN of the entry containing the group definition. */
053  private final DN groupDN;
054
055  /**
056   * The queue into which results will be placed while they are waiting to be
057   * returned.  The types of objects that may be placed in this queue are Entry
058   * objects to return or MembershipException objects to throw.
059   */
060  private final LinkedBlockingQueue<Object> resultQueue;
061
062  /** The search filter to use when filtering the set of group members. */
063  private final SearchFilter filter;
064
065  /** The search scope to use when filtering the set of group members. */
066  private final SearchScope scope;
067
068  /** The set of LDAP URLs that define the membership criteria. */
069  private final Set<LDAPURL> memberURLs;
070
071  /**
072   * Creates a new dynamic group member list with the provided information.
073   *
074   * @param  groupDN     The DN of the entry containing the group definition.
075   * @param  memberURLs  The set of LDAP URLs that define the membership
076   *                     criteria for the associated group.
077   *
078   * @throws  DirectoryException  If a problem occurs while creating the member
079   *                              list.
080   */
081  public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs)
082         throws DirectoryException
083  {
084    this(groupDN, memberURLs, null, null, null);
085  }
086
087  /**
088   * Creates a new dynamic group member list with the provided information.
089   *
090   * @param  groupDN     The DN of the entry containing the group definition.
091   * @param  memberURLs  The set of LDAP URLs that define the membership
092   *                     criteria for the associated group.
093   * @param  baseDN      The base DN that should be enforced for all entries to
094   *                     return.
095   * @param  scope       The scope that should be enforced for all entries to
096   *                     return.
097   * @param  filter      The filter that should be enforced for all entries to
098   *                     return.
099   *
100   * @throws  DirectoryException  If a problem occurs while creating the member
101   *                              list.
102   */
103  public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs,
104                                DN baseDN, SearchScope scope,
105                                SearchFilter filter)
106         throws DirectoryException
107  {
108    this.groupDN    = groupDN;
109    this.memberURLs = memberURLs;
110    this.baseDN     = baseDN;
111    this.filter     = filter;
112
113    if (scope == null)
114    {
115      this.scope = SearchScope.WHOLE_SUBTREE;
116    }
117    else
118    {
119      this.scope = scope;
120    }
121
122    searchesCompleted = false;
123    resultQueue = new LinkedBlockingQueue<>(10);
124
125    // We're going to have to perform one or more internal searches in order to
126    // get the results.  We need to be careful about the way that we construct
127    // them in order to avoid the possibility of getting duplicate results, so
128    // searches with overlapping bases will need to be combined.
129    LinkedHashMap<DN,LinkedList<LDAPURL>> baseDNs = new LinkedHashMap<>();
130    for (LDAPURL memberURL : memberURLs)
131    {
132      // First, determine the base DN for the search.  It needs to be evaluated
133      // as relative to both the overall base DN specified in the set of
134      // criteria, as well as any other existing base DNs in the same hierarchy.
135      DN urlBaseDN = memberURL.getBaseDN();
136      if (baseDN != null)
137      {
138        if (baseDN.isSubordinateOrEqualTo(urlBaseDN))
139        {
140          // The base DN requested by the user is below the base DN for this
141          // URL, so we'll use the base DN requested by the user.
142          urlBaseDN = baseDN;
143        }
144        else if (! urlBaseDN.isSubordinateOrEqualTo(baseDN))
145        {
146          // The base DN from the URL is outside the base requested by the user,
147          // so we can skip this URL altogether.
148          continue;
149        }
150      }
151
152      // If this is the first URL, then we can just add it with the base DN.
153      // Otherwise, we need to see if it needs to be merged with other URLs in
154      // the same hierarchy.
155      if (baseDNs.isEmpty())
156      {
157        baseDNs.put(urlBaseDN, newLinkedList(memberURL));
158      }
159      else
160      {
161        // See if the specified base DN is already in the map.  If so, then
162        // just add the new URL to the existing list.
163        LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN);
164        if (urlList == null)
165        {
166          // There's no existing list for the same base DN, but there might be
167          // DNs in an overlapping hierarchy.  If so, then use the base DN that
168          // is closest to the naming context.  If not, then add a new list with
169          // the current base DN.
170          boolean found = false;
171          Iterator<DN> iterator = baseDNs.keySet().iterator();
172          while (iterator.hasNext())
173          {
174            DN existingBaseDN = iterator.next();
175            if (urlBaseDN.isSubordinateOrEqualTo(existingBaseDN))
176            {
177              // The base DN for the current URL is below an existing base DN,
178              // so we can just add this URL to the existing list and be done.
179              urlList = baseDNs.get(existingBaseDN);
180              urlList.add(memberURL);
181              found = true;
182              break;
183            }
184            else if (existingBaseDN.isSubordinateOrEqualTo(urlBaseDN))
185            {
186              // The base DN for the current URL is above the existing base DN,
187              // so we should use the base DN for the current URL instead of the
188              // existing one.
189              urlList = baseDNs.get(existingBaseDN);
190              urlList.add(memberURL);
191              iterator.remove();
192              baseDNs.put(urlBaseDN, urlList);
193              found = true;
194              break;
195            }
196          }
197
198          if (! found)
199          {
200            baseDNs.put(urlBaseDN, newLinkedList(memberURL));
201          }
202        }
203        else
204        {
205          // There was already a list with the same base DN, so just add the URL.
206          urlList.add(memberURL);
207        }
208      }
209    }
210
211    // At this point, we should know what base DN(s) we need to use, so we can
212    // create the filter to use with that base DN.  There are some special-case
213    // optimizations that we can do here, but in general the filter will look
214    // like "(&(filter)(|(urlFilters)))".
215    LinkedHashMap<DN,SearchFilter> searchMap = new LinkedHashMap<>();
216    for (DN urlBaseDN : baseDNs.keySet())
217    {
218      LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN);
219      LinkedHashSet<SearchFilter> urlFilters = new LinkedHashSet<>();
220      for (LDAPURL url : urlList)
221      {
222        urlFilters.add(url.getFilter());
223      }
224
225      SearchFilter combinedFilter;
226      if (filter == null)
227      {
228        if (urlFilters.size() == 1)
229        {
230          combinedFilter = urlFilters.iterator().next();
231        }
232        else
233        {
234          combinedFilter = SearchFilter.createORFilter(urlFilters);
235        }
236      }
237      else
238      {
239        if (urlFilters.size() == 1)
240        {
241          SearchFilter urlFilter = urlFilters.iterator().next();
242          if (urlFilter.equals(filter))
243          {
244            combinedFilter = filter;
245          }
246          else
247          {
248            LinkedHashSet<SearchFilter> filterSet = new LinkedHashSet<>();
249            filterSet.add(filter);
250            filterSet.add(urlFilter);
251            combinedFilter = SearchFilter.createANDFilter(filterSet);
252          }
253        }
254        else
255        {
256          if (urlFilters.contains(filter))
257          {
258            combinedFilter = filter;
259          }
260          else
261          {
262            LinkedHashSet<SearchFilter> filterSet = new LinkedHashSet<>();
263            filterSet.add(filter);
264            filterSet.add(SearchFilter.createORFilter(urlFilters));
265            combinedFilter = SearchFilter.createANDFilter(filterSet);
266          }
267        }
268      }
269
270      searchMap.put(urlBaseDN, combinedFilter);
271    }
272
273    // At this point, we should have all the information we need to perform the
274    // searches.  Create arrays of the elements for each.
275    DN[]           baseDNArray = new DN[baseDNs.size()];
276    SearchFilter[] filterArray = new SearchFilter[baseDNArray.length];
277    LDAPURL[][]    urlArray    = new LDAPURL[baseDNArray.length][];
278    Iterator<DN> iterator = baseDNs.keySet().iterator();
279    for (int i=0; i < baseDNArray.length; i++)
280    {
281      baseDNArray[i] = iterator.next();
282      filterArray[i] = searchMap.get(baseDNArray[i]);
283
284      LinkedList<LDAPURL> urlList = baseDNs.get(baseDNArray[i]);
285      urlArray[i] = new LDAPURL[urlList.size()];
286      int j=0;
287      for (LDAPURL url : urlList)
288      {
289        urlArray[i][j++] = url;
290      }
291    }
292
293    DynamicGroupSearchThread searchThread =
294         new DynamicGroupSearchThread(this, baseDNArray, filterArray, urlArray);
295    searchThread.start();
296  }
297
298  /**
299   * Retrieves the DN of the dynamic group with which this dynamic group member
300   * list is associated.
301   *
302   * @return  The DN of the dynamic group with which this dynamic group member
303   *          list is associated.
304   */
305  public final DN getDynamicGroupDN()
306  {
307    return groupDN;
308  }
309
310  /**
311   * Indicates that all of the searches needed to iterate across the member list
312   * have completed and there will not be any more results provided.
313   */
314  final void setSearchesCompleted()
315  {
316    searchesCompleted = true;
317  }
318
319  /**
320   * Adds the provided entry to the set of results that should be returned for
321   * this member list.
322   *
323   * @param  entry  The entry to add to the set of results that should be
324   *                returned for this member list.
325   *
326   * @return  {@code true} if the entry was added to the result set, or
327   *          {@code false} if it was not (either because a timeout expired or
328   *          the attempt was interrupted).  If this method returns
329   *          {@code false}, then the search thread should terminate
330   *          immediately.
331   */
332  final boolean addResult(Entry entry)
333  {
334    try
335    {
336      return resultQueue.offer(entry, 10, TimeUnit.SECONDS);
337    }
338    catch (InterruptedException ie)
339    {
340      return false;
341    }
342  }
343
344  /**
345   * Adds the provided membership exception so that it will be thrown along with
346   * the set of results for this member list.
347   *
348   * @param  membershipException  The membership exception to be thrown.
349   *
350   * @return  {@code true} if the exception was added to the result set, or
351   *          {@code false} if it was not (either because a timeout expired or
352   *          the attempt was interrupted).  If this method returns
353   *          {@code false}, then the search thread should terminate
354   *          immediately.
355   */
356  final boolean addResult(MembershipException membershipException)
357  {
358    try
359    {
360      return resultQueue.offer(membershipException, 10, TimeUnit.SECONDS);
361    }
362    catch (InterruptedException ie)
363    {
364      return false;
365    }
366  }
367
368  @Override
369  public boolean hasMoreMembers()
370  {
371    while (! searchesCompleted)
372    {
373      if (resultQueue.peek() != null)
374      {
375        return true;
376      }
377
378      try
379      {
380        Thread.sleep(0, 1000);
381      } catch (Exception e) {}
382    }
383
384    return resultQueue.peek() != null;
385  }
386
387  @Override
388  public Entry nextMemberEntry()
389         throws MembershipException
390  {
391    if (! hasMoreMembers())
392    {
393      return null;
394    }
395
396    Object result = resultQueue.poll();
397    if (result == null)
398    {
399      close();
400      return null;
401    }
402    else if (result instanceof Entry)
403    {
404      return (Entry) result;
405    }
406    else if (result instanceof MembershipException)
407    {
408      MembershipException me = (MembershipException) result;
409      if (! me.continueIterating())
410      {
411        close();
412      }
413
414      throw me;
415    }
416
417    // We should never get here.
418    close();
419    return null;
420  }
421
422  @Override
423  public void close()
424  {
425    searchesCompleted = true;
426    resultQueue.clear();
427  }
428}