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