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