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