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