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-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2012-2015 ForgeRock AS
026 */
027package org.opends.guitools.controlpanel.browser;
028
029import static org.opends.messages.AdminToolMessages.*;
030
031import java.util.ArrayList;
032import java.util.Set;
033
034import javax.naming.InterruptedNamingException;
035import javax.naming.NameNotFoundException;
036import javax.naming.NamingEnumeration;
037import javax.naming.NamingException;
038import javax.naming.SizeLimitExceededException;
039import javax.naming.directory.SearchControls;
040import javax.naming.directory.SearchResult;
041import javax.naming.ldap.InitialLdapContext;
042import javax.naming.ldap.LdapName;
043import javax.swing.SwingUtilities;
044import javax.swing.tree.TreeNode;
045
046import org.forgerock.opendj.ldap.SearchScope;
047import org.opends.admin.ads.util.ConnectionUtils;
048import org.opends.guitools.controlpanel.ui.nodes.BasicNode;
049import org.opends.messages.AdminToolMessages;
050import org.opends.server.schema.SchemaConstants;
051import org.opends.server.types.DN;
052import org.opends.server.types.DirectoryException;
053import org.opends.server.types.LDAPURL;
054import org.opends.server.types.OpenDsException;
055import org.opends.server.types.RDN;
056
057/**
058 * The class that is in charge of doing the LDAP searches required to update a
059 * node: search the local entry, detect if it has children, retrieve the
060 * attributes required to render the node, etc.
061 */
062public class NodeRefresher extends AbstractNodeTask {
063  /** The enumeration containing all the states the refresher can have. */
064  public enum State
065  {
066    /** The refresher is queued, but not started. */
067    QUEUED,
068    /** The refresher is reading the local entry. */
069    READING_LOCAL_ENTRY,
070    /** The refresher is solving a referral. */
071    SOLVING_REFERRAL,
072    /** The refresher is detecting whether the entry has children or not. */
073    DETECTING_CHILDREN,
074    /** The refresher is searching for the children of the entry. */
075    SEARCHING_CHILDREN,
076    /** The refresher is finished. */
077    FINISHED,
078    /** The refresher is cancelled. */
079    CANCELLED,
080    /** The refresher has been interrupted. */
081    INTERRUPTED,
082    /** The refresher has failed. */
083    FAILED
084  }
085
086  BrowserController controller;
087  State state;
088  boolean recursive;
089
090  SearchResult localEntry;
091  SearchResult remoteEntry;
092  LDAPURL   remoteUrl;
093  boolean isLeafNode;
094  final ArrayList<SearchResult> childEntries = new ArrayList<>();
095  final boolean differential;
096  Exception exception;
097  Object exceptionArg;
098
099  /**
100   * The constructor of the refresher object.
101   * @param node the node on the tree to be updated.
102   * @param ctlr the BrowserController.
103   * @param localEntry the local entry corresponding to the node.
104   * @param recursive whether this task is recursive or not (children must be searched).
105   */
106  NodeRefresher(BasicNode node, BrowserController ctlr, SearchResult localEntry, boolean recursive) {
107    super(node);
108    controller = ctlr;
109    state = State.QUEUED;
110    this.recursive = recursive;
111
112    this.localEntry = localEntry;
113    differential = false;
114  }
115
116  /**
117   * Returns the local entry the refresher is handling.
118   * @return the local entry the refresher is handling.
119   */
120  public SearchResult getLocalEntry() {
121    return localEntry;
122  }
123
124  /**
125   * Returns the remote entry for the node.  It will be <CODE>null</CODE> if
126   * the entry is not a referral.
127   * @return the remote entry for the node.
128   */
129  public SearchResult getRemoteEntry() {
130    return remoteEntry;
131  }
132
133  /**
134   * Returns the URL of the remote entry.  It will be <CODE>null</CODE> if
135   * the entry is not a referral.
136   * @return the URL of the remote entry.
137   */
138  public LDAPURL getRemoteUrl() {
139    return remoteUrl;
140  }
141
142  /**
143   * Tells whether the node is a leaf or not.
144   * @return <CODE>true</CODE> if the node is a leaf and <CODE>false</CODE>
145   * otherwise.
146   */
147  public boolean isLeafNode() {
148    return isLeafNode;
149  }
150
151  /**
152   * Returns the child entries of the node.
153   * @return the child entries of the node.
154   */
155  public ArrayList<SearchResult> getChildEntries() {
156    return childEntries;
157  }
158
159  /**
160   * Returns whether this refresher object is working on differential mode or
161   * not.
162   * @return <CODE>true</CODE> if the refresher is working on differential
163   * mode and <CODE>false</CODE> otherwise.
164   */
165  public boolean isDifferential() {
166    return differential;
167  }
168
169  /**
170   * Returns the exception that occurred during the processing.  It returns
171   * <CODE>null</CODE> if no exception occurred.
172   * @return the exception that occurred during the processing.
173   */
174  public Exception getException() {
175    return exception;
176  }
177
178  /**
179   * Returns the argument of the exception that occurred during the processing.
180   * It returns <CODE>null</CODE> if no exception occurred or if the exception
181   * has no arguments.
182   * @return the argument exception that occurred during the processing.
183   */
184  public Object getExceptionArg() {
185    return exceptionArg;
186  }
187
188  /**
189   * Returns the displayed entry in the browser.  This depends on the
190   * visualization options in the BrowserController.
191   * @return the remote entry if the entry is a referral and the
192   * BrowserController is following referrals and the local entry otherwise.
193   */
194  public SearchResult getDisplayedEntry() {
195    SearchResult result;
196    if (controller.getFollowReferrals() && remoteEntry != null)
197    {
198      result = remoteEntry;
199    }
200    else {
201      result = localEntry;
202    }
203    return result;
204  }
205
206  /**
207   * Returns the LDAP URL of the displayed entry in the browser.  This depends
208   * on the visualization options in the BrowserController.
209   * @return the remote entry LDAP URL if the entry is a referral and the
210   * BrowserController is following referrals and the local entry LDAP URL
211   * otherwise.
212   */
213  public LDAPURL getDisplayedUrl() {
214    LDAPURL result;
215    if (controller.getFollowReferrals() && remoteUrl != null)
216    {
217      result = remoteUrl;
218    }
219    else {
220      result = controller.findUrlForLocalEntry(getNode());
221    }
222    return result;
223  }
224
225  /**
226   * Returns whether the refresh is over or not.
227   * @return <CODE>true</CODE> if the refresh is over and <CODE>false</CODE>
228   * otherwise.
229   */
230  public boolean isInFinalState() {
231    return state == State.FINISHED || state == State.CANCELLED || state == State.FAILED || state == State.INTERRUPTED;
232  }
233
234  /** The method that actually does the refresh. */
235  @Override
236  public void run() {
237    final BasicNode node = getNode();
238
239    try {
240      boolean checkExpand = false;
241      if (localEntry == null) {
242        changeStateTo(State.READING_LOCAL_ENTRY);
243        runReadLocalEntry();
244      }
245      if (!isInFinalState()) {
246        if (controller.getFollowReferrals() && isReferralEntry(localEntry)) {
247          changeStateTo(State.SOLVING_REFERRAL);
248          runSolveReferral();
249        }
250        if (node.isLeaf()) {
251          changeStateTo(State.DETECTING_CHILDREN);
252          runDetectChildren();
253        }
254        if (controller.nodeIsExpanded(node) && recursive) {
255          changeStateTo(State.SEARCHING_CHILDREN);
256          runSearchChildren();
257          /* If the node is not expanded, we have to refresh its children when we expand it */
258        } else if (recursive  && (!node.isLeaf() || !isLeafNode)) {
259          node.setRefreshNeededOnExpansion(true);
260          checkExpand = true;
261        }
262        changeStateTo(State.FINISHED);
263        if (checkExpand && mustAutomaticallyExpand(node))
264        {
265          SwingUtilities.invokeLater(new Runnable()
266          {
267            @Override
268            public void run()
269            {
270              controller.expandNode(node);
271            }
272          });
273        }
274      }
275    }
276    catch (NamingException ne)
277    {
278      exception = ne;
279      exceptionArg = null;
280    }
281    catch(SearchAbandonException x) {
282      exception = x.getException();
283      exceptionArg = x.getArg();
284      try {
285        changeStateTo(x.getState());
286      }
287      catch(SearchAbandonException xx) {
288        // We've done all what we can...
289      }
290    }
291  }
292
293  /**
294   * Tells whether a custom filter is being used (specified by the user in the
295   * browser dialog) or not.
296   * @return <CODE>true</CODE> if a custom filter is being used and
297   * <CODE>false</CODE> otherwise.
298   */
299  private boolean useCustomFilter()
300  {
301    boolean result=false;
302    if (controller.getFilter()!=null)
303    {
304      result =
305 !BrowserController.ALL_OBJECTS_FILTER.equals(controller.getFilter());
306    }
307    return result;
308  }
309
310  /**
311   * Performs the search in the case the user specified a custom filter.
312   * @param node the parent node we perform the search from.
313   * @param ctx the connection to be used.
314   * @throws NamingException if a problem occurred.
315   */
316  private void searchForCustomFilter(BasicNode node, InitialLdapContext ctx)
317  throws NamingException
318  {
319    SearchControls ctls = controller.getBasicSearchControls();
320    ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
321    ctls.setReturningAttributes(new String[] { SchemaConstants.NO_ATTRIBUTES });
322    ctls.setCountLimit(1);
323    NamingEnumeration<SearchResult> s = ctx.search(new LdapName(node.getDN()),
324              controller.getFilter(),
325              ctls);
326    try
327    {
328      if (!s.hasMore())
329      {
330        throw new NameNotFoundException("Entry "+node.getDN()+
331            " does not verify filter "+controller.getFilter());
332      }
333      while (s.hasMore())
334      {
335        s.next();
336      }
337    }
338    catch (SizeLimitExceededException slme)
339    {
340      // We are just searching for an entry, but if there is more than one
341      // this exception will be thrown.  We call sr.hasMore after the
342      // first entry has been retrieved to avoid sending a systematic
343      // abandon when closing the s NamingEnumeration.
344      // See CR 6976906.
345    }
346    finally
347    {
348      s.close();
349    }
350  }
351
352  /**
353   * Performs the search in the case the user specified a custom filter.
354   * @param dn the parent DN we perform the search from.
355   * @param ctx the connection to be used.
356   * @throws NamingException if a problem occurred.
357   */
358  private void searchForCustomFilter(String dn, InitialLdapContext ctx)
359  throws NamingException
360  {
361    SearchControls ctls = controller.getBasicSearchControls();
362    ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
363    ctls.setReturningAttributes(new String[]{});
364    ctls.setCountLimit(1);
365    NamingEnumeration<SearchResult> s = ctx.search(new LdapName(dn),
366              controller.getFilter(),
367              ctls);
368    try
369    {
370      if (!s.hasMore())
371      {
372        throw new NameNotFoundException("Entry "+dn+
373            " does not verify filter "+controller.getFilter());
374      }
375      while (s.hasMore())
376      {
377        s.next();
378      }
379    }
380    catch (SizeLimitExceededException slme)
381    {
382      // We are just searching for an entry, but if there is more than one
383      // this exception will be thrown.  We call sr.hasMore after the
384      // first entry has been retrieved to avoid sending a systematic
385      // abandon when closing the s NamingEnumeration.
386      // See CR 6976906.
387    }
388    finally
389    {
390      s.close();
391    }
392  }
393
394  /** Read the local entry associated to the current node. */
395  private void runReadLocalEntry() throws SearchAbandonException {
396    BasicNode node = getNode();
397    InitialLdapContext ctx = null;
398    try {
399      ctx = controller.findConnectionForLocalEntry(node);
400
401      if (ctx != null) {
402        if (useCustomFilter())
403        {
404          // Check that the entry verifies the filter
405          searchForCustomFilter(node, ctx);
406        }
407
408        SearchControls ctls = controller.getBasicSearchControls();
409        ctls.setReturningAttributes(controller.getAttrsForRedSearch());
410        ctls.setSearchScope(SearchControls.OBJECT_SCOPE);
411
412        NamingEnumeration<SearchResult> s =
413                ctx.search(new LdapName(node.getDN()),
414                controller.getObjectSearchFilter(),
415                ctls);
416        try
417        {
418          while (s.hasMore())
419          {
420            localEntry = s.next();
421            localEntry.setName(node.getDN());
422          }
423        }
424        finally
425        {
426          s.close();
427        }
428        if (localEntry == null) {
429          /* Not enough rights to read the entry or the entry simply does not exist */
430          throw new NameNotFoundException("Can't find entry: "+node.getDN());
431        }
432        throwAbandonIfNeeded(null);
433      } else {
434          changeStateTo(State.FINISHED);
435      }
436    }
437    catch(NamingException x) {
438        throwAbandonIfNeeded(x);
439    }
440    finally {
441      if (ctx != null) {
442        controller.releaseLDAPConnection(ctx);
443      }
444    }
445  }
446
447  /**
448   * Solve the referral associated to the current node.
449   * This routine assumes that node.getReferral() is non null
450   * and that BrowserController.getFollowReferrals() == true.
451   * It also protect the browser against looping referrals by
452   * limiting the number of hops.
453   * @throws SearchAbandonException if the hop count limit for referrals has
454   * been exceeded.
455   * @throws NamingException if an error occurred searching the entry.
456   */
457  private void runSolveReferral()
458  throws SearchAbandonException, NamingException {
459    int hopCount = 0;
460    String[] referral = getNode().getReferral();
461    while (referral != null && hopCount < 10)
462    {
463      readRemoteEntry(referral);
464      referral = BrowserController.getReferral(remoteEntry);
465      hopCount++;
466    }
467    if (referral != null)
468    {
469      throwAbandonIfNeeded(new ReferralLimitExceededException(
470          AdminToolMessages.ERR_REFERRAL_LIMIT_EXCEEDED.get(hopCount)));
471    }
472  }
473
474  /**
475   * Searches for the remote entry.
476   * @param referral the referral list to be used to search the remote entry.
477   * @throws SearchAbandonException if an error occurs.
478   */
479  private void readRemoteEntry(String[] referral)
480  throws SearchAbandonException {
481    LDAPConnectionPool connectionPool = controller.getConnectionPool();
482    LDAPURL url = null;
483    SearchResult entry = null;
484    String remoteDn = null;
485    Exception lastException = null;
486    Object lastExceptionArg = null;
487
488    int i = 0;
489    while (i < referral.length && entry == null)
490    {
491      InitialLdapContext ctx = null;
492      try {
493        url = LDAPURL.decode(referral[i], false);
494        if (url.getHost() == null)
495        {
496          // Use the local server connection.
497          ctx = controller.getUserDataConnection();
498          url.setHost(ConnectionUtils.getHostName(ctx));
499          url.setPort(ConnectionUtils.getPort(ctx));
500          url.setScheme(ConnectionUtils.isSSL(ctx)?"ldaps":"ldap");
501        }
502        ctx = connectionPool.getConnection(url);
503        remoteDn = url.getRawBaseDN();
504        if (remoteDn == null || "".equals(remoteDn))
505        {
506          /* The referral has not a target DN specified: we
507             have to use the DN of the entry that contains the
508             referral... */
509          if (remoteEntry != null) {
510            remoteDn = remoteEntry.getName();
511          } else {
512            remoteDn = localEntry.getName();
513          }
514          /* We have to recreate the url including the target DN we are using */
515          url = new LDAPURL(url.getScheme(), url.getHost(), url.getPort(),
516              remoteDn, url.getAttributes(), url.getScope(), url.getRawFilter(),
517                 url.getExtensions());
518        }
519        if (useCustomFilter() && url.getScope() == SearchScope.BASE_OBJECT)
520        {
521          // Check that the entry verifies the filter
522          searchForCustomFilter(remoteDn, ctx);
523        }
524
525        int scope = getJNDIScope(url);
526        String filter = getJNDIFilter(url);
527
528        SearchControls ctls = controller.getBasicSearchControls();
529        ctls.setReturningAttributes(controller.getAttrsForBlackSearch());
530        ctls.setSearchScope(scope);
531        ctls.setCountLimit(1);
532        NamingEnumeration<SearchResult> sr = ctx.search(remoteDn,
533            filter,
534            ctls);
535        try
536        {
537          boolean found = false;
538          while (sr.hasMore())
539          {
540            entry = sr.next();
541            String name;
542            if (entry.getName().length() == 0)
543            {
544              name = remoteDn;
545            }
546            else
547            {
548              name = unquoteRelativeName(entry.getName())+","+remoteDn;
549            }
550            entry.setName(name);
551            found = true;
552          }
553          if (!found)
554          {
555            throw new NameNotFoundException();
556          }
557        }
558        catch (SizeLimitExceededException sle)
559        {
560          // We are just searching for an entry, but if there is more than one
561          // this exception will be thrown.  We call sr.hasMore after the
562          // first entry has been retrieved to avoid sending a systematic
563          // abandon when closing the sr NamingEnumeration.
564          // See CR 6976906.
565        }
566        finally
567        {
568          sr.close();
569        }
570        throwAbandonIfNeeded(null);
571      }
572      catch (InterruptedNamingException x) {
573        throwAbandonIfNeeded(x);
574      }
575      catch (NamingException | DirectoryException x) {
576        lastException = x;
577        lastExceptionArg = referral[i];
578      }
579      finally {
580        if (ctx != null) {
581          connectionPool.releaseConnection(ctx);
582        }
583      }
584      i = i + 1;
585    }
586    if (entry == null) {
587      throw new SearchAbandonException(
588          State.FAILED, lastException, lastExceptionArg);
589    }
590    else
591    {
592      if (url.getScope() != SearchScope.BASE_OBJECT)
593      {
594        // The URL is to be transformed: the code assumes that the URL points
595        // to the remote entry.
596        url = new LDAPURL(url.getScheme(), url.getHost(),
597            url.getPort(), entry.getName(), url.getAttributes(),
598            SearchScope.BASE_OBJECT, null, url.getExtensions());
599      }
600      checkLoopInReferral(url, referral[i-1]);
601      remoteUrl = url;
602      remoteEntry = entry;
603    }
604  }
605
606  /**
607   * Tells whether the provided node must be automatically expanded or not.
608   * This is used when the user provides a custom filter, in this case we
609   * expand automatically the tree.
610   * @param node the node to analyze.
611   * @return <CODE>true</CODE> if the node must be expanded and
612   * <CODE>false</CODE> otherwise.
613   */
614  private boolean mustAutomaticallyExpand(BasicNode node)
615  {
616    boolean mustAutomaticallyExpand = false;
617    if (controller.isAutomaticExpand())
618    {
619      // Limit the number of expansion levels to 3
620      int nLevels = 0;
621      TreeNode parent = node;
622      while (parent != null)
623      {
624        nLevels ++;
625        parent = parent.getParent();
626      }
627      mustAutomaticallyExpand = nLevels <= 4;
628    }
629    return mustAutomaticallyExpand;
630  }
631
632  /**
633   * Detects whether the entries has children or not.
634   * @throws SearchAbandonException if the search was abandoned.
635   * @throws NamingException if an error during the search occurred.
636   */
637  private void runDetectChildren()
638  throws SearchAbandonException, NamingException {
639    if (controller.isShowContainerOnly() || !isNumSubOrdinatesUsable()) {
640      runDetectChildrenManually();
641    }
642    else {
643      SearchResult entry = getDisplayedEntry();
644      isLeafNode = !BrowserController.getHasSubOrdinates(entry);
645    }
646  }
647
648  /**
649   * Detects whether the entry has children by performing a search using the
650   * entry as base DN.
651   * @throws SearchAbandonException if there is an error.
652   */
653  private void runDetectChildrenManually() throws SearchAbandonException {
654    BasicNode parentNode = getNode();
655    InitialLdapContext ctx = null;
656    NamingEnumeration<SearchResult> searchResults = null;
657
658    try {
659      // We set the search constraints so that only one entry is returned.
660      // It's enough to know if the entry has children or not.
661      SearchControls ctls = controller.getBasicSearchControls();
662      ctls.setCountLimit(1);
663      ctls.setReturningAttributes(
664          new String[] { SchemaConstants.NO_ATTRIBUTES });
665      if (useCustomFilter())
666      {
667        ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
668      }
669      else
670      {
671        ctls.setSearchScope(SearchControls.OBJECT_SCOPE);
672      }
673      // Send an LDAP search
674      ctx = controller.findConnectionForDisplayedEntry(parentNode);
675      searchResults = ctx.search(
676          new LdapName(controller.findBaseDNForChildEntries(parentNode)),
677          controller.getChildSearchFilter(),
678          ctls);
679
680      throwAbandonIfNeeded(null);
681      isLeafNode = true;
682      // Check if parentNode has children
683      while (searchResults.hasMoreElements()) {
684        isLeafNode = false;
685      }
686    }
687    catch (SizeLimitExceededException e)
688    {
689      // We are just searching for an entry, but if there is more than one
690      // this exception will be thrown.  We call sr.hasMore after the
691      // first entry has been retrieved to avoid sending a systematic
692      // abandon when closing the searchResults NamingEnumeration.
693      // See CR 6976906.
694    }
695    catch (NamingException x) {
696      throwAbandonIfNeeded(x);
697    }
698    finally {
699      if (ctx != null) {
700        controller.releaseLDAPConnection(ctx);
701      }
702      if (searchResults != null)
703      {
704        try
705        {
706          searchResults.close();
707        }
708        catch (NamingException x)
709        {
710          throwAbandonIfNeeded(x);
711        }
712      }
713    }
714  }
715
716  /**
717   * NUMSUBORDINATE HACK
718   * numsubordinates is not usable if the displayed entry
719   * is listed in in the hacker.
720   * Note: *usable* means *usable for detecting children presence*.
721   */
722  private boolean isNumSubOrdinatesUsable() throws NamingException {
723    SearchResult entry = getDisplayedEntry();
724    boolean hasSubOrdinates = BrowserController.getHasSubOrdinates(entry);
725    if (!hasSubOrdinates)
726    {
727      LDAPURL url = getDisplayedUrl();
728      return !controller.getNumSubordinateHacker().contains(url);
729    }
730    // Other values are usable
731    return true;
732  }
733
734  /**
735   * Searches for the children.
736   * @throws SearchAbandonException if an error occurs.
737   */
738  private void runSearchChildren() throws SearchAbandonException {
739    InitialLdapContext ctx = null;
740    BasicNode parentNode = getNode();
741    parentNode.setSizeLimitReached(false);
742
743    try {
744      // Send an LDAP search
745      SearchControls ctls = controller.getBasicSearchControls();
746      if (useCustomFilter())
747      {
748        ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
749      }
750      else
751      {
752        ctls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
753      }
754      ctls.setReturningAttributes(controller.getAttrsForRedSearch());
755      ctx = controller.findConnectionForDisplayedEntry(parentNode);
756      String parentDn = controller.findBaseDNForChildEntries(parentNode);
757      int parentComponents;
758      try
759      {
760        DN dn = DN.valueOf(parentDn);
761        parentComponents = dn.size();
762      }
763      catch (Throwable t)
764      {
765        throw new RuntimeException("Error decoding dn: "+parentDn+" . "+t,
766            t);
767      }
768      NamingEnumeration<SearchResult> entries = ctx.search(
769            new LdapName(parentDn),
770                controller.getChildSearchFilter(),
771                ctls);
772
773      try
774      {
775        while (entries.hasMore())
776        {
777          SearchResult r = entries.next();
778          String name;
779          if (r.getName().length() == 0)
780          {
781            continue;
782          }
783          else
784          {
785            name = unquoteRelativeName(r.getName())+","+parentDn;
786          }
787          boolean add = false;
788          if (useCustomFilter())
789          {
790            // Check that is an immediate child: use a faster method by just
791            // comparing the number of components.
792            DN dn = null;
793            try
794            {
795              dn = DN.valueOf(name);
796              add = dn.size() == parentComponents + 1;
797            }
798            catch (Throwable t)
799            {
800              throw new RuntimeException("Error decoding dns: "+t, t);
801            }
802
803            if (!add)
804            {
805              // Is not a direct child.  Check if the parent has been added,
806              // if it is the case, do not add the parent.  If is not the case,
807              // search for the parent and add it.
808              RDN[] rdns = new RDN[parentComponents + 1];
809              int diff = dn.size() - rdns.length;
810              for (int i=0; i < rdns.length; i++)
811              {
812                rdns[i] = dn.getRDN(i + diff);
813              }
814              final DN parentToAddDN = new DN(rdns);
815              boolean mustAddParent = true;
816              for (SearchResult addedEntry : childEntries)
817              {
818                try
819                {
820                  DN addedDN = DN.valueOf(addedEntry.getName());
821                  if (addedDN.equals(parentToAddDN))
822                  {
823                    mustAddParent = false;
824                    break;
825                  }
826                }
827                catch (Throwable t)
828                {
829                  throw new RuntimeException("Error decoding dn: "+
830                      addedEntry.getName()+" . "+t, t);
831                }
832              }
833              if (mustAddParent)
834              {
835                final boolean resultValue[] = {true};
836                // Check the children added to the tree
837                try
838                {
839                  SwingUtilities.invokeAndWait(new Runnable()
840                  {
841                    @Override
842                    public void run()
843                    {
844                      for (int i=0; i<getNode().getChildCount(); i++)
845                      {
846                        BasicNode node = (BasicNode)getNode().getChildAt(i);
847                        try
848                        {
849                          DN dn = DN.valueOf(node.getDN());
850                          if (dn.equals(parentToAddDN))
851                          {
852                            resultValue[0] = false;
853                            break;
854                          }
855                        }
856                        catch (Throwable t)
857                        {
858                          throw new RuntimeException("Error decoding dn: "+
859                              node.getDN()+" . "+t, t);
860                        }
861                      }
862                    }
863                  });
864                }
865                catch (Throwable t)
866                {
867                  // Ignore
868                }
869                mustAddParent = resultValue[0];
870              }
871              if (mustAddParent)
872              {
873                SearchResult parentResult = searchManuallyEntry(ctx,
874                    parentToAddDN.toString());
875                childEntries.add(parentResult);
876              }
877            }
878          }
879          else
880          {
881            add = true;
882          }
883          if (add)
884          {
885            r.setName(name);
886            childEntries.add(r);
887            // Time to time we update the display
888            if (childEntries.size() >= 20) {
889              changeStateTo(State.SEARCHING_CHILDREN);
890              childEntries.clear();
891            }
892          }
893          throwAbandonIfNeeded(null);
894        }
895      }
896      finally
897      {
898        entries.close();
899      }
900    }
901    catch (SizeLimitExceededException slee)
902    {
903      parentNode.setSizeLimitReached(true);
904    }
905    catch (NamingException x) {
906      throwAbandonIfNeeded(x);
907    }
908    finally {
909      if (ctx != null)
910      {
911        controller.releaseLDAPConnection(ctx);
912      }
913    }
914  }
915
916  /**
917   * Returns the entry for the given dn.
918   * The code assumes that the request controls are set in the connection.
919   * @param ctx the connection to be used.
920   * @param dn the DN of the entry to be searched.
921   * @throws NamingException if an error occurs.
922   */
923  private SearchResult searchManuallyEntry(InitialLdapContext ctx, String dn)
924  throws NamingException
925  {
926    SearchResult sr = null;
927//  Send an LDAP search
928    SearchControls ctls = controller.getBasicSearchControls();
929    ctls.setSearchScope(SearchControls.OBJECT_SCOPE);
930    ctls.setReturningAttributes(controller.getAttrsForRedSearch());
931    NamingEnumeration<SearchResult> entries = ctx.search(
932          new LdapName(dn),
933              controller.getObjectSearchFilter(),
934              ctls);
935
936    try
937    {
938      while (entries.hasMore())
939      {
940        sr = entries.next();
941        sr.setName(dn);
942      }
943    }
944    finally
945    {
946      entries.close();
947    }
948    return sr;
949  }
950
951  /** Utilities. */
952
953  /**
954   * Change the state of the task and inform the BrowserController.
955   * @param newState the new state for the refresher.
956   */
957  private void changeStateTo(State newState) throws SearchAbandonException {
958    State oldState = state;
959    state = newState;
960    try {
961      controller.invokeRefreshTaskDidProgress(this, oldState, newState);
962    }
963    catch(InterruptedException x) {
964      throwAbandonIfNeeded(x);
965    }
966  }
967
968  /**
969   * Transform an exception into a TaskAbandonException.
970   * If no exception is passed, the routine checks if the task has
971   * been canceled and throws an TaskAbandonException accordingly.
972   * @param x the exception.
973   * @throws SearchAbandonException if the task/refresher must be abandoned.
974   */
975  private void throwAbandonIfNeeded(Exception x) throws SearchAbandonException {
976    SearchAbandonException tax = null;
977    if (x != null) {
978      if (x instanceof InterruptedException || x instanceof InterruptedNamingException)
979      {
980        tax = new SearchAbandonException(State.INTERRUPTED, x, null);
981      }
982      else {
983        tax = new SearchAbandonException(State.FAILED, x, null);
984      }
985    }
986    else if (isCanceled()) {
987      tax = new SearchAbandonException(State.CANCELLED, null, null);
988    }
989    if (tax != null) {
990      throw tax;
991    }
992  }
993
994  /**
995   * Removes the quotes surrounding the provided name.  JNDI can return relative
996   * names with this format.
997   * @param name the relative name to be treated.
998   * @return an String representing the provided relative name without
999   * surrounding quotes.
1000   */
1001  private String unquoteRelativeName(String name)
1002  {
1003    if (name.length() > 0 && name.charAt(0) == '"')
1004    {
1005      if (name.charAt(name.length() - 1) == '"')
1006      {
1007        return name.substring(1, name.length() - 1);
1008      }
1009      else
1010      {
1011        return name.substring(1);
1012      }
1013    }
1014    else
1015    {
1016      return name;
1017    }
1018  }
1019
1020  /** DEBUG : Dump the state of the task. */
1021  void dump() {
1022    System.out.println("=============");
1023    System.out.println("         node: " + getNode().getDN());
1024    System.out.println("    recursive: " + recursive);
1025    System.out.println(" differential: " + differential);
1026
1027    System.out.println("        state: " + state);
1028    System.out.println("   localEntry: " + localEntry);
1029    System.out.println("  remoteEntry: " + remoteEntry);
1030    System.out.println("    remoteUrl: " + remoteUrl);
1031    System.out.println("   isLeafNode: " + isLeafNode);
1032    System.out.println("    exception: " + exception);
1033    System.out.println(" exceptionArg: " + exceptionArg);
1034    System.out.println("=============");
1035  }
1036
1037  /**
1038   * Checks that the entry's objectClass contains 'referral' and that the
1039   * attribute 'ref' is present.
1040   * @param entry the search result.
1041   * @return <CODE>true</CODE> if the entry's objectClass contains 'referral'
1042   * and the attribute 'ref' is present and <CODE>false</CODE> otherwise.
1043   * @throws NamingException if an error occurs.
1044   */
1045  static boolean isReferralEntry(SearchResult entry) throws NamingException {
1046    boolean result = false;
1047    Set<String> ocValues = ConnectionUtils.getValues(entry, "objectClass");
1048    if (ocValues != null) {
1049      for (String value : ocValues)
1050      {
1051        boolean isReferral = "referral".equalsIgnoreCase(value);
1052
1053        if (isReferral) {
1054          result = ConnectionUtils.getFirstValue(entry, "ref") != null;
1055          break;
1056        }
1057      }
1058    }
1059    return result;
1060  }
1061
1062  /**
1063   * Returns the scope to be used in a JNDI request based on the information
1064   * of an LDAP URL.
1065   * @param url the LDAP URL.
1066   * @return the scope to be used in a JNDI request.
1067   */
1068  private int getJNDIScope(LDAPURL url)
1069  {
1070    int scope;
1071    if (url.getScope() != null)
1072    {
1073      switch (url.getScope().asEnum())
1074      {
1075      case BASE_OBJECT:
1076        scope = SearchControls.OBJECT_SCOPE;
1077        break;
1078      case WHOLE_SUBTREE:
1079        scope = SearchControls.SUBTREE_SCOPE;
1080        break;
1081      case SUBORDINATES:
1082        scope = SearchControls.ONELEVEL_SCOPE;
1083        break;
1084      case SINGLE_LEVEL:
1085        scope = SearchControls.ONELEVEL_SCOPE;
1086        break;
1087      default:
1088        scope = SearchControls.OBJECT_SCOPE;
1089      }
1090    }
1091    else
1092    {
1093      scope = SearchControls.OBJECT_SCOPE;
1094    }
1095    return scope;
1096  }
1097
1098  /**
1099   * Returns the filter to be used in a JNDI request based on the information
1100   * of an LDAP URL.
1101   * @param url the LDAP URL.
1102   * @return the filter.
1103   */
1104  private String getJNDIFilter(LDAPURL url)
1105  {
1106    String filter = url.getRawFilter();
1107    if (filter == null)
1108    {
1109      filter = controller.getObjectSearchFilter();
1110    }
1111    return filter;
1112  }
1113
1114  /**
1115   * Check that there is no loop in terms of DIT (the check basically identifies
1116   * whether we are pointing to an entry above in the same server).
1117   * @param url the URL to the remote entry.  It is assumed that the base DN
1118   * of the URL points to the remote entry.
1119   * @param referral the referral used to retrieve the remote entry.
1120   * @throws SearchAbandonException if there is a loop issue (the remoteEntry
1121   * is actually an entry in the same server as the local entry but above in the
1122   * DIT).
1123   */
1124  private void checkLoopInReferral(LDAPURL url,
1125      String referral) throws SearchAbandonException
1126  {
1127    boolean checkSucceeded = true;
1128    try
1129    {
1130      DN dn1 = DN.valueOf(getNode().getDN());
1131      DN dn2 = url.getBaseDN();
1132      if (dn2.isAncestorOf(dn1))
1133      {
1134        String host = url.getHost();
1135        int port = url.getPort();
1136        String adminHost = ConnectionUtils.getHostName(
1137            controller.getConfigurationConnection());
1138        int adminPort =
1139          ConnectionUtils.getPort(controller.getConfigurationConnection());
1140        checkSucceeded = port != adminPort ||
1141        !adminHost.equalsIgnoreCase(host);
1142
1143        if (checkSucceeded)
1144        {
1145          String hostUserData = ConnectionUtils.getHostName(
1146              controller.getUserDataConnection());
1147          int portUserData =
1148            ConnectionUtils.getPort(controller.getUserDataConnection());
1149          checkSucceeded = port != portUserData ||
1150          !hostUserData.equalsIgnoreCase(host);
1151        }
1152      }
1153    }
1154    catch (OpenDsException odse)
1155    {
1156      // Ignore
1157    }
1158    if (!checkSucceeded)
1159    {
1160      throw new SearchAbandonException(
1161          State.FAILED, new ReferralLimitExceededException(
1162              ERR_CTRL_PANEL_REFERRAL_LOOP.get(url.getRawBaseDN())), referral);
1163    }
1164  }
1165}