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
073  /**
074   * Creates a new dynamic group member list with the provided information.
075   *
076   * @param  groupDN     The DN of the entry containing the group definition.
077   * @param  memberURLs  The set of LDAP URLs that define the membership
078   *                     criteria for the associated group.
079   *
080   * @throws  DirectoryException  If a problem occurs while creating the member
081   *                              list.
082   */
083  public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs)
084         throws DirectoryException
085  {
086    this(groupDN, memberURLs, null, null, null);
087  }
088
089
090
091  /**
092   * Creates a new dynamic group member list with the provided information.
093   *
094   * @param  groupDN     The DN of the entry containing the group definition.
095   * @param  memberURLs  The set of LDAP URLs that define the membership
096   *                     criteria for the associated group.
097   * @param  baseDN      The base DN that should be enforced for all entries to
098   *                     return.
099   * @param  scope       The scope that should be enforced for all entries to
100   *                     return.
101   * @param  filter      The filter that should be enforced for all entries to
102   *                     return.
103   *
104   * @throws  DirectoryException  If a problem occurs while creating the member
105   *                              list.
106   */
107  public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs,
108                                DN baseDN, SearchScope scope,
109                                SearchFilter filter)
110         throws DirectoryException
111  {
112    this.groupDN    = groupDN;
113    this.memberURLs = memberURLs;
114    this.baseDN     = baseDN;
115    this.filter     = filter;
116
117    if (scope == null)
118    {
119      this.scope = SearchScope.WHOLE_SUBTREE;
120    }
121    else
122    {
123      this.scope = scope;
124    }
125
126    searchesCompleted = false;
127    resultQueue = new LinkedBlockingQueue<>(10);
128
129
130    // We're going to have to perform one or more internal searches in order to
131    // get the results.  We need to be careful about the way that we construct
132    // them in order to avoid the possibility of getting duplicate results, so
133    // searches with overlapping bases will need to be combined.
134    LinkedHashMap<DN,LinkedList<LDAPURL>> baseDNs = new LinkedHashMap<>();
135    for (LDAPURL memberURL : memberURLs)
136    {
137      // First, determine the base DN for the search.  It needs to be evaluated
138      // as relative to both the overall base DN specified in the set of
139      // criteria, as well as any other existing base DNs in the same hierarchy.
140      DN urlBaseDN = memberURL.getBaseDN();
141      if (baseDN != null)
142      {
143        if (baseDN.isSubordinateOrEqualTo(urlBaseDN))
144        {
145          // The base DN requested by the user is below the base DN for this
146          // URL, so we'll use the base DN requested by the user.
147          urlBaseDN = baseDN;
148        }
149        else if (! urlBaseDN.isSubordinateOrEqualTo(baseDN))
150        {
151          // The base DN from the URL is outside the base requested by the user,
152          // so we can skip this URL altogether.
153          continue;
154        }
155      }
156
157      // If this is the first URL, then we can just add it with the base DN.
158      // Otherwise, we need to see if it needs to be merged with other URLs in
159      // the same hierarchy.
160      if (baseDNs.isEmpty())
161      {
162        baseDNs.put(urlBaseDN, newLinkedList(memberURL));
163      }
164      else
165      {
166        // See if the specified base DN is already in the map.  If so, then
167        // just add the new URL to the existing list.
168        LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN);
169        if (urlList == null)
170        {
171          // There's no existing list for the same base DN, but there might be
172          // DNs in an overlapping hierarchy.  If so, then use the base DN that
173          // is closest to the naming context.  If not, then add a new list with
174          // the current base DN.
175          boolean found = false;
176          Iterator<DN> iterator = baseDNs.keySet().iterator();
177          while (iterator.hasNext())
178          {
179            DN existingBaseDN = iterator.next();
180            if (urlBaseDN.isSubordinateOrEqualTo(existingBaseDN))
181            {
182              // The base DN for the current URL is below an existing base DN,
183              // so we can just add this URL to the existing list and be done.
184              urlList = baseDNs.get(existingBaseDN);
185              urlList.add(memberURL);
186              found = true;
187              break;
188            }
189            else if (existingBaseDN.isSubordinateOrEqualTo(urlBaseDN))
190            {
191              // The base DN for the current URL is above the existing base DN,
192              // so we should use the base DN for the current URL instead of the
193              // existing one.
194              urlList = baseDNs.get(existingBaseDN);
195              urlList.add(memberURL);
196              iterator.remove();
197              baseDNs.put(urlBaseDN, urlList);
198              found = true;
199              break;
200            }
201          }
202
203          if (! found)
204          {
205            baseDNs.put(urlBaseDN, newLinkedList(memberURL));
206          }
207        }
208        else
209        {
210          // There was already a list with the same base DN, so just add the URL.
211          urlList.add(memberURL);
212        }
213      }
214    }
215
216
217    // At this point, we should know what base DN(s) we need to use, so we can
218    // create the filter to use with that base DN.  There are some special-case
219    // optimizations that we can do here, but in general the filter will look
220    // like "(&(filter)(|(urlFilters)))".
221    LinkedHashMap<DN,SearchFilter> searchMap = new LinkedHashMap<>();
222    for (DN urlBaseDN : baseDNs.keySet())
223    {
224      LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN);
225      LinkedHashSet<SearchFilter> urlFilters = new LinkedHashSet<>();
226      for (LDAPURL url : urlList)
227      {
228        urlFilters.add(url.getFilter());
229      }
230
231      SearchFilter combinedFilter;
232      if (filter == null)
233      {
234        if (urlFilters.size() == 1)
235        {
236          combinedFilter = urlFilters.iterator().next();
237        }
238        else
239        {
240          combinedFilter = SearchFilter.createORFilter(urlFilters);
241        }
242      }
243      else
244      {
245        if (urlFilters.size() == 1)
246        {
247          SearchFilter urlFilter = urlFilters.iterator().next();
248          if (urlFilter.equals(filter))
249          {
250            combinedFilter = filter;
251          }
252          else
253          {
254            LinkedHashSet<SearchFilter> filterSet = new LinkedHashSet<>();
255            filterSet.add(filter);
256            filterSet.add(urlFilter);
257            combinedFilter = SearchFilter.createANDFilter(filterSet);
258          }
259        }
260        else
261        {
262          if (urlFilters.contains(filter))
263          {
264            combinedFilter = filter;
265          }
266          else
267          {
268            LinkedHashSet<SearchFilter> filterSet = new LinkedHashSet<>();
269            filterSet.add(filter);
270            filterSet.add(SearchFilter.createORFilter(urlFilters));
271            combinedFilter = SearchFilter.createANDFilter(filterSet);
272          }
273        }
274      }
275
276      searchMap.put(urlBaseDN, combinedFilter);
277    }
278
279
280    // At this point, we should have all the information we need to perform the
281    // searches.  Create arrays of the elements for each.
282    DN[]           baseDNArray = new DN[baseDNs.size()];
283    SearchFilter[] filterArray = new SearchFilter[baseDNArray.length];
284    LDAPURL[][]    urlArray    = new LDAPURL[baseDNArray.length][];
285    Iterator<DN> iterator = baseDNs.keySet().iterator();
286    for (int i=0; i < baseDNArray.length; i++)
287    {
288      baseDNArray[i] = iterator.next();
289      filterArray[i] = searchMap.get(baseDNArray[i]);
290
291      LinkedList<LDAPURL> urlList = baseDNs.get(baseDNArray[i]);
292      urlArray[i] = new LDAPURL[urlList.size()];
293      int j=0;
294      for (LDAPURL url : urlList)
295      {
296        urlArray[i][j++] = url;
297      }
298    }
299
300
301    DynamicGroupSearchThread searchThread =
302         new DynamicGroupSearchThread(this, baseDNArray, filterArray, urlArray);
303    searchThread.start();
304  }
305
306
307
308  /**
309   * Retrieves the DN of the dynamic group with which this dynamic group member
310   * list is associated.
311   *
312   * @return  The DN of the dynamic group with which this dynamic group member
313   *          list is associated.
314   */
315  public final DN getDynamicGroupDN()
316  {
317    return groupDN;
318  }
319
320
321
322  /**
323   * Indicates that all of the searches needed to iterate across the member list
324   * have completed and there will not be any more results provided.
325   */
326  final void setSearchesCompleted()
327  {
328    searchesCompleted = true;
329  }
330
331
332
333  /**
334   * Adds the provided entry to the set of results that should be returned for
335   * this member list.
336   *
337   * @param  entry  The entry to add to the set of results that should be
338   *                returned for this member list.
339   *
340   * @return  {@code true} if the entry was added to the result set, or
341   *          {@code false} if it was not (either because a timeout expired or
342   *          the attempt was interrupted).  If this method returns
343   *          {@code false}, then the search thread should terminate
344   *          immediately.
345   */
346  final boolean addResult(Entry entry)
347  {
348    try
349    {
350      return resultQueue.offer(entry, 10, TimeUnit.SECONDS);
351    }
352    catch (InterruptedException ie)
353    {
354      return false;
355    }
356  }
357
358
359
360  /**
361   * Adds the provided membership exception so that it will be thrown along with
362   * the set of results for this member list.
363   *
364   * @param  membershipException  The membership exception to be thrown.
365   *
366   * @return  {@code true} if the exception was added to the result set, or
367   *          {@code false} if it was not (either because a timeout expired or
368   *          the attempt was interrupted).  If this method returns
369   *          {@code false}, then the search thread should terminate
370   *          immediately.
371   */
372  final boolean addResult(MembershipException membershipException)
373  {
374    try
375    {
376      return resultQueue.offer(membershipException, 10, TimeUnit.SECONDS);
377    }
378    catch (InterruptedException ie)
379    {
380      return false;
381    }
382  }
383
384
385
386  /** {@inheritDoc} */
387  @Override
388  public boolean hasMoreMembers()
389  {
390    while (! searchesCompleted)
391    {
392      if (resultQueue.peek() != null)
393      {
394        return true;
395      }
396
397      try
398      {
399        Thread.sleep(0, 1000);
400      } catch (Exception e) {}
401    }
402
403    return resultQueue.peek() != null;
404  }
405
406
407
408  /** {@inheritDoc} */
409  @Override
410  public Entry nextMemberEntry()
411         throws MembershipException
412  {
413    if (! hasMoreMembers())
414    {
415      return null;
416    }
417
418    Object result = resultQueue.poll();
419    if (result == null)
420    {
421      close();
422      return null;
423    }
424    else if (result instanceof Entry)
425    {
426      return (Entry) result;
427    }
428    else if (result instanceof MembershipException)
429    {
430      MembershipException me = (MembershipException) result;
431      if (! me.continueIterating())
432      {
433        close();
434      }
435
436      throw me;
437    }
438
439    // We should never get here.
440    close();
441    return null;
442  }
443
444
445
446  /** {@inheritDoc} */
447  @Override
448  public void close()
449  {
450    searchesCompleted = true;
451    resultQueue.clear();
452  }
453}
454