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 2014-2015 ForgeRock AS.
016 */
017package org.opends.guitools.controlpanel.browser;
018
019import java.awt.Font;
020import java.io.IOException;
021import java.lang.reflect.InvocationTargetException;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Enumeration;
025import java.util.List;
026import java.util.Set;
027import java.util.SortedSet;
028import java.util.TreeSet;
029import java.util.logging.Level;
030import java.util.logging.Logger;
031
032import javax.naming.NameNotFoundException;
033import javax.naming.NamingException;
034import javax.naming.directory.SearchControls;
035import javax.naming.directory.SearchResult;
036import javax.naming.ldap.Control;
037import javax.naming.ldap.InitialLdapContext;
038import javax.naming.ldap.ManageReferralControl;
039import javax.naming.ldap.SortControl;
040import javax.naming.ldap.SortKey;
041import javax.swing.Icon;
042import javax.swing.JTree;
043import javax.swing.SwingUtilities;
044import javax.swing.event.TreeExpansionEvent;
045import javax.swing.event.TreeExpansionListener;
046import javax.swing.tree.DefaultTreeModel;
047import javax.swing.tree.TreeNode;
048import javax.swing.tree.TreePath;
049
050import org.opends.admin.ads.ADSContext;
051import org.opends.admin.ads.util.ConnectionUtils;
052import org.opends.guitools.controlpanel.datamodel.CustomSearchResult;
053import org.opends.guitools.controlpanel.datamodel.ServerDescriptor;
054import org.opends.guitools.controlpanel.event.BrowserEvent;
055import org.opends.guitools.controlpanel.event.BrowserEventListener;
056import org.opends.guitools.controlpanel.event.ReferralAuthenticationListener;
057import org.opends.guitools.controlpanel.ui.nodes.BasicNode;
058import org.opends.guitools.controlpanel.ui.nodes.BrowserNodeInfo;
059import org.opends.guitools.controlpanel.ui.nodes.RootNode;
060import org.opends.guitools.controlpanel.ui.nodes.SuffixNode;
061import org.opends.guitools.controlpanel.ui.renderer.BrowserCellRenderer;
062import org.opends.guitools.controlpanel.util.NumSubordinateHacker;
063import org.opends.guitools.controlpanel.util.Utilities;
064import org.opends.server.config.ConfigConstants;
065import org.opends.server.types.LDAPURL;
066
067import static org.opends.server.util.ServerConstants.*;
068
069/**
070 * This is the main class of the LDAP entry browser.  It is in charge of
071 * updating a tree that is passed as parameter.  Every instance of
072 * BrowserController is associated with a unique JTree.
073 * The different visualization options are passed to BrowserController using
074 * some setter and getter methods (the user can specify for instance whether
075 * the entries must be sorted or not).
076 */
077public class BrowserController
078implements TreeExpansionListener, ReferralAuthenticationListener
079{
080  /**
081   * The mask used to display the number of ACIs or not.
082   */
083  private static final int DISPLAY_ACI_COUNT = 0x01;
084
085  /**
086   * The list of attributes that are used to sort the entries (if the sorting
087   * option is used).
088   */
089  private static final String[] SORT_ATTRIBUTES =
090      { "cn", "givenname", "o", "ou", "sn", "uid" };
091
092  /**
093   * This is a key value.  It is used to specify that the attribute that should
094   * be used to display the entry is the RDN attribute.
095   */
096  private static final String RDN_ATTRIBUTE = "rdn attribute";
097
098  /**
099   * The filter used to retrieve all the entries.
100   */
101  public static final String ALL_OBJECTS_FILTER =
102    "(|(objectClass=*)(objectClass=ldapsubentry))";
103
104  private static final String NUMSUBORDINATES_ATTR = "numsubordinates";
105  private static final String HASSUBORDINATES_ATTR = "hassubordinates";
106  private static final String ACI_ATTR = "aci";
107
108  private final JTree tree;
109  private final DefaultTreeModel treeModel;
110  private final RootNode rootNode;
111  private int displayFlags;
112  private String displayAttribute;
113  private final boolean showAttributeName;
114  private InitialLdapContext ctxConfiguration;
115  private InitialLdapContext ctxUserData;
116  private boolean followReferrals;
117  private boolean sorted;
118  private boolean showContainerOnly;
119  private boolean automaticExpand;
120  private boolean automaticallyExpandedNode;
121  private String[] containerClasses;
122  private NumSubordinateHacker numSubordinateHacker;
123  private int queueTotalSize;
124  private int maxChildren;
125  private final Collection<BrowserEventListener> listeners = new ArrayList<>();
126  private final LDAPConnectionPool connectionPool;
127  private final IconPool iconPool;
128
129  private final NodeSearcherQueue refreshQueue;
130
131  private String filter;
132
133  private static final Logger LOG =
134    Logger.getLogger(BrowserController.class.getName());
135
136  /**
137   * Constructor of the BrowserController.
138   * @param tree the tree that must be updated.
139   * @param cpool the connection pool object that will provide the connections
140   * to be used.
141   * @param ipool the icon pool to be used to retrieve the icons that will be
142   * used to render the nodes in the tree.
143   */
144  public BrowserController(JTree tree, LDAPConnectionPool cpool,
145      IconPool ipool)
146  {
147    this.tree = tree;
148    iconPool = ipool;
149    rootNode = new RootNode();
150    rootNode.setIcon(iconPool.getIconForRootNode());
151    treeModel = new DefaultTreeModel(rootNode);
152    tree.setModel(treeModel);
153    tree.addTreeExpansionListener(this);
154    tree.setCellRenderer(new BrowserCellRenderer());
155    displayFlags = DISPLAY_ACI_COUNT;
156    showAttributeName = false;
157    displayAttribute = RDN_ATTRIBUTE;
158    followReferrals = false;
159    sorted = false;
160    showContainerOnly = true;
161    containerClasses = new String[0];
162    queueTotalSize = 0;
163    connectionPool = cpool;
164    connectionPool.addReferralAuthenticationListener(this);
165
166    refreshQueue = new NodeSearcherQueue("New red", 2);
167
168    // NUMSUBORDINATE HACK
169    // Create an empty hacker to avoid null value test.
170    // However this value will be overridden by full hacker.
171    numSubordinateHacker = new NumSubordinateHacker();
172  }
173
174
175  /**
176   * Set the connection for accessing the directory.  Since we must use
177   * different controls when searching the configuration and the user data,
178   * two connections must be provided (this is done to avoid synchronization
179   * issues).  We also pass the server descriptor corresponding to the
180   * connections to have a proper rendering of the root node.
181   * @param server the server descriptor.
182   * @param ctxConfiguration the connection to be used to retrieve the data in
183   * the configuration base DNs.
184   * @param ctxUserData the connection to be used to retrieve the data in the
185   * user base DNs.
186   * @throws NamingException if an error occurs.
187   */
188  public void setConnections(
189      ServerDescriptor server,
190      InitialLdapContext ctxConfiguration,
191      InitialLdapContext ctxUserData) throws NamingException {
192    String rootNodeName;
193    if (ctxConfiguration != null)
194    {
195      this.ctxConfiguration = ctxConfiguration;
196      this.ctxUserData = ctxUserData;
197
198      this.ctxConfiguration.setRequestControls(
199          getConfigurationRequestControls());
200      this.ctxUserData.setRequestControls(getRequestControls());
201      rootNodeName = server.getHostname() + ":" +
202      ConnectionUtils.getPort(ctxConfiguration);
203    }
204    else {
205      rootNodeName = "";
206    }
207    rootNode.setDisplayName(rootNodeName);
208    startRefresh(null);
209  }
210
211
212  /**
213   * Return the connection for accessing the directory configuration.
214   * @return the connection for accessing the directory configuration.
215   */
216  public InitialLdapContext getConfigurationConnection() {
217    return ctxConfiguration;
218  }
219
220  /**
221   * Return the connection for accessing the directory user data.
222   * @return the connection for accessing the directory user data.
223   */
224  public InitialLdapContext getUserDataConnection() {
225    return ctxUserData;
226  }
227
228
229  /**
230   * Return the JTree controlled by this controller.
231   * @return the JTree controlled by this controller.
232   */
233  public JTree getTree() {
234    return tree;
235  }
236
237
238  /**
239   * Return the connection pool used by this controller.
240   * If a client class adds authentication to the connection
241   * pool, it must inform the controller by calling notifyAuthDataChanged().
242   * @return the connection pool used by this controller.
243   */
244  public LDAPConnectionPool getConnectionPool() {
245    return  connectionPool;
246  }
247
248  /**
249   * Return the icon pool used by this controller.
250   * @return the icon pool used by this controller.
251   */
252  public IconPool getIconPool() {
253    return  iconPool;
254  }
255
256  /**
257   * Tells whether the given suffix is in the tree or not.
258   * @param suffixDn the DN of the suffix to be analyzed.
259   * @return <CODE>true</CODE> if the provided String is the DN of a suffix
260   * and <CODE>false</CODE> otherwise.
261   * @throws IllegalArgumentException if a node with the given dn exists but
262   * is not a suffix node.
263   */
264  public boolean hasSuffix(String suffixDn) throws IllegalArgumentException
265  {
266    return findSuffixNode(suffixDn, rootNode) != null;
267  }
268
269  /**
270   * Add an LDAP suffix to this controller.
271   * A new node is added in the JTree and a refresh is started.
272   * @param suffixDn the DN of the suffix.
273   * @param parentSuffixDn the DN of the parent suffix (or <CODE>null</CODE> if
274   * there is no parent DN).
275   * @return the TreePath of the new node.
276   * @throws IllegalArgumentException if a node with the given dn exists.
277   */
278  public TreePath addSuffix(String suffixDn, String parentSuffixDn)
279  throws IllegalArgumentException
280  {
281    SuffixNode parentNode;
282    if (parentSuffixDn != null) {
283      parentNode = findSuffixNode(parentSuffixDn, rootNode);
284      if (parentNode == null) {
285        throw new IllegalArgumentException("Invalid suffix dn " +
286            parentSuffixDn);
287      }
288    }
289    else {
290      parentNode = rootNode;
291    }
292    int index = findChildNode(parentNode, suffixDn);
293    if (index >= 0) { // A node has alreay this dn -> bug
294      throw new IllegalArgumentException("Duplicate suffix dn " + suffixDn);
295    }
296    index = -(index + 1);
297    SuffixNode newNode = new SuffixNode(suffixDn);
298    treeModel.insertNodeInto(newNode, parentNode, index);
299    startRefreshNode(newNode, null, true);
300
301    return new TreePath(treeModel.getPathToRoot(newNode));
302  }
303
304  /**
305   * Add an LDAP suffix to this controller.
306   * A new node is added in the JTree and a refresh is started.
307   * @param nodeDn the DN of the node to be added.
308   * @return the TreePath of the new node.
309   */
310  public TreePath addNodeUnderRoot(String nodeDn) {
311    SuffixNode parentNode = rootNode;
312    int index = findChildNode(parentNode, nodeDn);
313    if (index >= 0) { // A node has already this dn -> bug
314      throw new IllegalArgumentException("Duplicate node dn " + nodeDn);
315    }
316    index = -(index + 1);
317    BasicNode newNode = new BasicNode(nodeDn);
318    treeModel.insertNodeInto(newNode, parentNode, index);
319    startRefreshNode(newNode, null, true);
320
321    return new TreePath(treeModel.getPathToRoot(newNode));
322  }
323
324
325  /**
326   * Remove all the suffixes.
327   * The controller removes all the nodes from the JTree except the root.
328   * @return the TreePath of the root node.
329   */
330  public TreePath removeAllUnderRoot() {
331    stopRefresh();
332    removeAllChildNodes(rootNode, false /* Delete suffixes */);
333    return new TreePath(treeModel.getPathToRoot(rootNode));
334  }
335
336
337  /**
338   * Return the display flags.
339   * @return the display flags.
340   */
341  public int getDisplayFlags() {
342    return displayFlags;
343  }
344
345
346  /**
347   * Set the display flags and call startRefresh().
348   * @param flags the display flags to be set.
349   */
350  public void setDisplayFlags(int flags) {
351    displayFlags = flags;
352    startRefresh(null);
353  }
354
355  /**
356   * Set the display attribute (the attribute that will be used to retrieve
357   * the string that will appear in the tree when rendering the node).
358   * This routine collapses the JTree and invokes startRefresh().
359   * @param displayAttribute the display attribute to be used.
360   */
361  public void setDisplayAttribute(String displayAttribute) {
362    this.displayAttribute = displayAttribute;
363    stopRefresh();
364    removeAllChildNodes(rootNode, true /* Keep suffixes */);
365    startRefresh(null);
366  }
367
368  /**
369   * Returns the attribute used to display the entry.
370   * RDN_ATTRIBUTE is the rdn is used.
371   * @return the attribute used to display the entry.
372   */
373  public String getDisplayAttribute() {
374    return displayAttribute;
375  }
376
377  /**
378   * Says whether we are showing the attribute name or not.
379   * @return <CODE>true</CODE> if we are showing the attribute name and
380   * <CODE>false</CODE> otherwise.
381   */
382  public boolean isAttributeNameShown() {
383    return showAttributeName;
384  }
385
386  /**
387   * Sets the maximum number of children to display for a node.
388   * 0 if there is no limit
389   * @param maxChildren the maximum number of children to display for a node.
390   */
391  public void setMaxChildren(int maxChildren) {
392    this.maxChildren = maxChildren;
393  }
394
395  /**
396   * Return the maximum number of children to display.
397   * @return the maximum number of children to display.
398   */
399  public int getMaxChildren() {
400    return maxChildren;
401  }
402
403  /**
404   * Return true if this controller follows referrals.
405   * @return <CODE>true</CODE> if this controller follows referrals and
406   * <CODE>false</CODE> otherwise.
407   */
408  public boolean getFollowReferrals() {
409    return followReferrals;
410  }
411
412
413  /**
414   * Enable/display the following of referrals.
415   * This routine starts a refresh on each referral node.
416   * @param followReferrals whether to follow referrals or not.
417   * @throws NamingException if there is an error updating the request controls
418   * of the internal connections.
419   */
420  public void setFollowReferrals(boolean followReferrals) throws NamingException
421  {
422    this.followReferrals = followReferrals;
423    stopRefresh();
424    removeAllChildNodes(rootNode, true /* Keep suffixes */);
425    ctxConfiguration.setRequestControls(getConfigurationRequestControls());
426    ctxUserData.setRequestControls(getRequestControls());
427    connectionPool.setRequestControls(getRequestControls());
428    startRefresh(null);
429  }
430
431
432  /**
433   * Return true if entries are displayed sorted.
434   * @return <CODE>true</CODE> if entries are displayed sorted and
435   * <CODE>false</CODE> otherwise.
436   */
437  public boolean isSorted() {
438    return sorted;
439  }
440
441
442  /**
443   * Enable/disable entry sort.
444   * This routine collapses the JTree and invokes startRefresh().
445   * @param sorted whether to sort the entries or not.
446   * @throws NamingException if there is an error updating the request controls
447   * of the internal connections.
448   */
449  public void setSorted(boolean sorted) throws NamingException {
450    stopRefresh();
451    removeAllChildNodes(rootNode, true /* Keep suffixes */);
452    this.sorted = sorted;
453    ctxConfiguration.setRequestControls(getConfigurationRequestControls());
454    ctxUserData.setRequestControls(getRequestControls());
455    connectionPool.setRequestControls(getRequestControls());
456    startRefresh(null);
457  }
458
459
460  /**
461   * Return true if only container entries are displayed.
462   * An entry is a container if:
463   *    - it has some children
464   *    - or its class is one of the container classes
465   *      specified with setContainerClasses().
466   * @return <CODE>true</CODE> if only container entries are displayed and
467   * <CODE>false</CODE> otherwise.
468   */
469  public boolean isShowContainerOnly() {
470    return showContainerOnly;
471  }
472
473
474  /**
475   * Enable or disable container display and call startRefresh().
476   * @param showContainerOnly whether to display only containers or all the
477   * entries.
478   */
479  public void setShowContainerOnly(boolean showContainerOnly) {
480    this.showContainerOnly = showContainerOnly;
481    startRefresh(null);
482  }
483
484
485  /**
486   * Find the BrowserNodeInfo associated to a TreePath and returns
487   * the describing IBrowserNodeInfo.
488   * @param path the TreePath associated with the node we are searching.
489   * @return the BrowserNodeInfo associated to the TreePath.
490   */
491  public BrowserNodeInfo getNodeInfoFromPath(TreePath path) {
492    BasicNode node = (BasicNode)path.getLastPathComponent();
493    return new BrowserNodeInfoImpl(node);
494  }
495
496
497  /**
498   * Return the array of container classes for this controller.
499   * Warning: the returned array is not cloned.
500   * @return the array of container classes for this controller.
501   */
502  public String[] getContainerClasses() {
503    return containerClasses;
504  }
505
506
507  /**
508   * Set the list of container classes and calls startRefresh().
509   * Warning: the array is not cloned.
510   * @param containerClasses the lis of container classes.
511   */
512  public void setContainerClasses(String[] containerClasses) {
513    this.containerClasses = containerClasses;
514    startRefresh(null);
515  }
516
517
518  /**
519   * NUMSUBORDINATE HACK
520   * Make the hacker public so that RefreshTask can use it.
521   * @return the NumSubordinateHacker object used by the controller.
522   */
523  public NumSubordinateHacker getNumSubordinateHacker() {
524    return numSubordinateHacker;
525  }
526
527
528  /**
529   * NUMSUBORDINATE HACK
530   * Set the hacker. Note this method does not trigger any
531   * refresh. The caller is supposed to do it afterward.
532   * @param h the  NumSubordinateHacker.
533   */
534  public void setNumSubordinateHacker(NumSubordinateHacker h) {
535    if (h == null) {
536      throw new IllegalArgumentException("hacker cannot be null");
537    }
538    numSubordinateHacker = h;
539  }
540
541  /**
542   * Add a BrowserEventListener to this controller.
543   * @param l the listener to be added.
544   */
545  public void addBrowserEventListener(BrowserEventListener l) {
546    listeners.add(l);
547  }
548
549  /**
550   * Notify this controller that an entry has been added.
551   * The controller adds a new node in the JTree and starts refreshing this new
552   * node.
553   * This routine returns the tree path about the new entry.
554   * @param parentInfo the parent node of the entry added.
555   * @param newEntryDn the dn of the entry to be added.
556   * @return the tree path associated with the new entry.
557   */
558  public TreePath notifyEntryAdded(BrowserNodeInfo parentInfo,
559      String newEntryDn) {
560    BasicNode parentNode = parentInfo.getNode();
561    BasicNode childNode = new BasicNode(newEntryDn);
562    int childIndex;
563    if (sorted) {
564      childIndex = findChildNode(parentNode, newEntryDn);
565      if (childIndex >= 0) {
566        throw new IllegalArgumentException("Duplicate DN " + newEntryDn);
567      }
568      childIndex = -(childIndex + 1);
569    }
570    else {
571      childIndex = parentNode.getChildCount();
572    }
573    parentNode.setLeaf(false);
574    treeModel.insertNodeInto(childNode, parentNode, childIndex);
575    startRefreshNode(childNode, null, false);
576    return new TreePath(treeModel.getPathToRoot(childNode));
577  }
578
579
580  /**
581   * Notify this controller that a entry has been deleted.
582   * The controller removes the corresponding node from the JTree and returns
583   * the TreePath of the parent node.
584   * @param nodeInfo the node to be deleted.
585   * @return the tree path associated with the parent of the deleted node.
586   */
587  public TreePath notifyEntryDeleted(BrowserNodeInfo nodeInfo) {
588    BasicNode node = nodeInfo.getNode();
589    if (node == rootNode) {
590      throw new IllegalArgumentException("Root node cannot be removed");
591    }
592
593    /* If the parent is null... the node is no longer in the tree */
594    final TreeNode parentNode = node.getParent();
595    if (parentNode != null) {
596      removeOneNode(node);
597      return new TreePath(treeModel.getPathToRoot(parentNode));
598    }
599    return null;
600  }
601
602
603  /**
604   * Notify this controller that an entry has changed.
605   * The controller starts refreshing the corresponding node.
606   * Child nodes are not refreshed.
607   * @param nodeInfo the node that changed.
608   */
609  public void notifyEntryChanged(BrowserNodeInfo nodeInfo) {
610    BasicNode node = nodeInfo.getNode();
611    startRefreshNode(node, null, false);
612  }
613
614  /**
615   * Notify this controller that authentication data have changed in the
616   * connection pool.
617   */
618  @Override
619  public void notifyAuthDataChanged() {
620    notifyAuthDataChanged(null);
621  }
622
623  /**
624   * Notify this controller that authentication data have changed in the
625   * connection pool for the specified url.
626   * The controller starts refreshing the node which represent entries from the
627   * url.
628   * @param url the URL of the connection that changed.
629   */
630  private void notifyAuthDataChanged(LDAPURL url) {
631    // TODO: temporary implementation
632    //    we should refresh only nodes :
633    //    - whose URL matches 'url'
634    //    - whose errorType == ERROR_SOLVING_REFERRAL and
635    //      errorArg == url
636    startRefreshReferralNodes(rootNode);
637  }
638
639
640  /**
641   * Start a refresh from the specified node.
642   * If some refresh are on-going on descendant nodes, they are stopped.
643   * If nodeInfo is null, refresh is started from the root.
644   * @param nodeInfo the node to be refreshed.
645   */
646  public void startRefresh(BrowserNodeInfo nodeInfo) {
647    BasicNode node;
648    if (nodeInfo == null) {
649      node = rootNode;
650    }
651    else {
652      node = nodeInfo.getNode();
653    }
654    stopRefreshNode(node);
655    startRefreshNode(node, null, true);
656  }
657
658  /**
659   * Stop the current refreshing.
660   * Nodes being expanded are collapsed.
661   */
662  private void stopRefresh() {
663    stopRefreshNode(rootNode);
664    // TODO: refresh must be stopped in a clean state.
665  }
666
667  /**
668   * Start refreshing the whole tree from the specified node.
669   * We queue a refresh which:
670   *    - updates the base node
671   *    - is recursive
672   * @param node the parent node that will be refreshed.
673   * @param localEntry the local entry corresponding to the node.
674   * @param recursive whether the refresh must be executed recursively or not.
675   */
676  private void startRefreshNode(BasicNode node, SearchResult localEntry,
677      boolean recursive) {
678    if (node == rootNode) {
679      // For the root node, readBaseEntry is meaningless.
680      if (recursive) {
681        // The root cannot be queued directly.
682        // We need to queue each child individually.
683        Enumeration<?> e = rootNode.children();
684        while (e.hasMoreElements()) {
685          BasicNode child = (BasicNode)e.nextElement();
686          startRefreshNode(child, null, true);
687        }
688      }
689    }
690    else {
691      refreshQueue.queue(new NodeRefresher(node, this, localEntry, recursive));
692      // The task does not *see* suffixes.
693      // So we need to propagate the refresh on
694      // the sub-suffixes if any.
695      if (recursive && node instanceof SuffixNode) {
696        Enumeration<?> e = node.children();
697        while (e.hasMoreElements()) {
698          BasicNode child = (BasicNode)e.nextElement();
699          if (child instanceof SuffixNode) {
700            startRefreshNode(child, null, true);
701          }
702        }
703      }
704    }
705  }
706
707
708
709
710  /**
711   * Stop refreshing below this node.
712   * TODO: this method is very costly when applied to something else than the
713   * root node.
714   * @param node the node where the refresh must stop.
715   */
716  private void stopRefreshNode(BasicNode node) {
717    if (node == rootNode) {
718      refreshQueue.cancelAll();
719    }
720    else {
721      Enumeration<?> e = node.children();
722      while (e.hasMoreElements()) {
723        BasicNode child = (BasicNode)e.nextElement();
724        stopRefreshNode(child);
725      }
726      refreshQueue.cancelForNode(node);
727    }
728  }
729
730
731
732  /**
733   * Call startRefreshNode() on each referral node accessible from parentNode.
734   * @param parentNode the parent node.
735   */
736  private void startRefreshReferralNodes(BasicNode parentNode) {
737    Enumeration<?> e = parentNode.children();
738    while (e.hasMoreElements()) {
739      BasicNode child = (BasicNode)e.nextElement();
740      if (child.getReferral() != null || child.getRemoteUrl() != null) {
741        startRefreshNode(child, null, true);
742      }
743      else {
744        startRefreshReferralNodes(child);
745      }
746    }
747  }
748
749
750
751  /**
752   * Remove all the children below parentNode *without changing the leaf state*.
753   * If specified, it keeps the SuffixNode and recurses on them. Inform the tree
754   * model.
755   * @param parentNode the parent node.
756   * @param keepSuffixes whether the suffixes should be kept or not.
757   */
758  private void removeAllChildNodes(BasicNode parentNode, boolean keepSuffixes) {
759    for (int i = parentNode.getChildCount() - 1; i >= 0; i--) {
760      BasicNode child = (BasicNode)parentNode.getChildAt(i);
761      if (child instanceof SuffixNode && keepSuffixes) {
762        removeAllChildNodes(child, true);
763        child.setRefreshNeededOnExpansion(true);
764      }
765      else {
766        child.removeFromParent();
767      }
768    }
769    treeModel.nodeStructureChanged(parentNode);
770  }
771
772  /**
773   * For BrowserController private use.  When a node is expanded, refresh it
774   * if it needs it (to search the children for instance).
775   * @param event the tree expansion event.
776   */
777  @Override
778  public void treeExpanded(TreeExpansionEvent event) {
779    if (!automaticallyExpandedNode)
780    {
781      automaticExpand = false;
782    }
783    BasicNode basicNode = (BasicNode)event.getPath().getLastPathComponent();
784    if (basicNode.isRefreshNeededOnExpansion()) {
785      basicNode.setRefreshNeededOnExpansion(false);
786      // Starts a recursive refresh which does not read the base entry
787      startRefreshNode(basicNode, null, true);
788    }
789  }
790
791
792  /**
793   * For BrowserController private use.  When a node is collapsed the refresh
794   * tasks on it are canceled.
795   * @param event the tree collapse event.
796   */
797  @Override
798  public void treeCollapsed(TreeExpansionEvent event) {
799    Object node = event.getPath().getLastPathComponent();
800    if (!(node instanceof RootNode)) {
801      BasicNode basicNode = (BasicNode)node;
802      stopRefreshNode(basicNode);
803      synchronized (refreshQueue)
804      {
805        boolean isWorking = refreshQueue.isWorking(basicNode);
806        refreshQueue.cancelForNode(basicNode);
807        if (isWorking)
808        {
809          basicNode.setRefreshNeededOnExpansion(true);
810        }
811      }
812    }
813  }
814
815  /**
816   * Sets which is the inspected node.  This method simply marks the selected
817   * node in the tree so that it can have a different rendering.  This is
818   * useful for instance when the right panel has a list of entries to which
819   * the menu action apply, to make a difference between the selected node in
820   * the tree (to which the action in the main menu will not apply) and the
821   * selected nodes in the right pane.
822   * @param node the selected node.
823   */
824  public void setInspectedNode(BrowserNodeInfo node) {
825    BrowserCellRenderer renderer = (BrowserCellRenderer)tree.getCellRenderer();
826    if (node == null) {
827      renderer.setInspectedNode(null);
828    } else {
829      renderer.setInspectedNode(node.getNode());
830    }
831  }
832
833
834  /**
835   * Routines for the task classes
836   * =============================
837   *
838   * Note that these routines only read controller variables.
839   * They do not alter any variable: so they can be safely
840   * called by task threads without synchronize clauses.
841   */
842
843
844  /**
845   * The tree model created by the controller and assigned
846   * to the JTree.
847   * @return the tree model.
848   */
849  public DefaultTreeModel getTreeModel() {
850    return treeModel;
851  }
852
853  /**
854   * Sets the filter that must be used by the browser controller to retrieve
855   * entries.
856   * @param filter the LDAP filter.
857   */
858  public void setFilter(String filter)
859  {
860    this.filter = filter;
861  }
862
863  /**
864   * Returns the filter that is being used to search the entries.
865   * @return the filter that is being used to search the entries.
866   */
867  public String getFilter()
868  {
869    return filter;
870  }
871
872  /**
873   * Returns the filter used to make a object base search.
874   * @return the filter used to make a object base search.
875   */
876  String getObjectSearchFilter()
877  {
878    return ALL_OBJECTS_FILTER;
879  }
880
881
882  /**
883   * Return the LDAP search filter to use for searching child entries.
884   * If showContainerOnly is true, the filter will select only the
885   * container entries. If not, the filter will select all the children.
886   * @return the LDAP search filter to use for searching child entries.
887   */
888  String getChildSearchFilter() {
889    String result;
890    if (showContainerOnly) {
891      if (followReferrals) {
892        /* In the case we are following referrals, we have to consider referrals
893         as nodes.
894         Suppose the following scenario: a referral points to a remote entry
895         that has children (node), BUT the referral entry in the local server
896         has no children.  It won't be included in the filter and it won't
897         appear in the tree.  But what we are displaying is the remote entry,
898         the result is that we have a NODE that does not appear in the tree and
899         so the user cannot browse it.
900
901         This has some side effects:
902         If we cannot follow the referral, a leaf will appear on the tree (as it
903         if were a node).
904         If the referral points to a leaf entry, a leaf will appear on the tree
905         (as if it were a node).
906
907         This is minor compared to the impossibility of browsing a subtree with
908         the NODE/LEAF layout.
909         */
910        result = "(|(&(hasSubordinates=true)"+filter+")(objectClass=referral)";
911      } else {
912        result = "(|(&(hasSubordinates=true)"+filter+")";
913      }
914      for (String containerClass : containerClasses)
915      {
916        result += "(objectClass=" + containerClass + ")";
917      }
918      result += ")";
919    }
920    else {
921      result = filter;
922    }
923
924    return result;
925  }
926
927
928
929
930  /**
931   * Return the LDAP connection to reading the base entry of a node.
932   * @param node the node for which we want the LDAP connection.
933   * @throws NamingException if there is an error retrieving the connection.
934   * @return the LDAP connection to reading the base entry of a node.
935   */
936  InitialLdapContext findConnectionForLocalEntry(BasicNode node)
937  throws NamingException {
938    return findConnectionForLocalEntry(node, isConfigurationNode(node));
939  }
940
941  /**
942   * Return the LDAP connection to reading the base entry of a node.
943   * @param node the node for which we want toe LDAP connection.
944   * @param isConfigurationNode whether the node is a configuration node or not.
945   * @throws NamingException if there is an error retrieving the connection.
946   * @return the LDAP connection to reading the base entry of a node.
947   */
948  private InitialLdapContext findConnectionForLocalEntry(BasicNode node,
949      boolean isConfigurationNode) throws NamingException
950  {
951    if (node == rootNode) {
952      return ctxConfiguration;
953    }
954
955    final BasicNode parent = (BasicNode) node.getParent();
956    if (parent != null && parent != rootNode)
957    {
958      return findConnectionForDisplayedEntry(parent, isConfigurationNode);
959    }
960    return isConfigurationNode ? ctxConfiguration : ctxUserData;
961  }
962
963  /**
964   * Returns whether a given node is a configuration node or not.
965   * @param node the node to analyze.
966   * @return <CODE>true</CODE> if the node is a configuration node and
967   * <CODE>false</CODE> otherwise.
968   */
969  public boolean isConfigurationNode(BasicNode node)
970  {
971    if (node instanceof RootNode)
972    {
973      return true;
974    }
975    if (node instanceof SuffixNode)
976    {
977      String dn = node.getDN();
978      return Utilities.areDnsEqual(dn, ADSContext.getAdministrationSuffixDN()) ||
979          Utilities.areDnsEqual(dn, ConfigConstants.DN_DEFAULT_SCHEMA_ROOT) ||
980          Utilities.areDnsEqual(dn, ConfigConstants.DN_TASK_ROOT) ||
981          Utilities.areDnsEqual(dn, ConfigConstants.DN_CONFIG_ROOT) ||
982          Utilities.areDnsEqual(dn, ConfigConstants.DN_MONITOR_ROOT) ||
983          Utilities.areDnsEqual(dn, ConfigConstants.DN_TRUST_STORE_ROOT) ||
984          Utilities.areDnsEqual(dn, ConfigConstants.DN_BACKUP_ROOT) ||
985          Utilities.areDnsEqual(dn, DN_EXTERNAL_CHANGELOG_ROOT);
986    }
987    else
988    {
989      BasicNode parentNode = (BasicNode)node.getParent();
990      return isConfigurationNode(parentNode);
991    }
992  }
993
994  /**
995   * Return the LDAP connection to search the displayed entry (which can be the
996   * local or remote entry).
997   * @param node the node for which we want toe LDAP connection.
998   * @return the LDAP connection to search the displayed entry.
999   * @throws NamingException if there is an error retrieving the connection.
1000   */
1001  public InitialLdapContext findConnectionForDisplayedEntry(BasicNode node)
1002  throws NamingException {
1003    return findConnectionForDisplayedEntry(node, isConfigurationNode(node));
1004  }
1005
1006
1007  /**
1008   * Return the LDAP connection to search the displayed entry (which can be the
1009   * local or remote entry).
1010   * @param node the node for which we want toe LDAP connection.
1011   * @param isConfigurationNode whether the node is a configuration node or not.
1012   * @return the LDAP connection to search the displayed entry.
1013   * @throws NamingException if there is an error retrieving the connection.
1014   */
1015  private InitialLdapContext findConnectionForDisplayedEntry(BasicNode node,
1016      boolean isConfigurationNode) throws NamingException {
1017    if (followReferrals && node.getRemoteUrl() != null)
1018    {
1019      return connectionPool.getConnection(node.getRemoteUrl());
1020    }
1021    return findConnectionForLocalEntry(node, isConfigurationNode);
1022  }
1023
1024
1025
1026  /**
1027   * Release a connection returned by selectConnectionForChildEntries() or
1028   * selectConnectionForBaseEntry().
1029   * @param ctx the connection to be released.
1030   */
1031  void releaseLDAPConnection(InitialLdapContext ctx) {
1032    if (ctx != this.ctxConfiguration && ctx != this.ctxUserData)
1033    {
1034      // Thus it comes from the connection pool
1035      connectionPool.releaseConnection(ctx);
1036    }
1037  }
1038
1039
1040  /**
1041   * Returns the local entry URL for a given node.
1042   * @param node the node.
1043   * @return the local entry URL for a given node.
1044   */
1045  LDAPURL findUrlForLocalEntry(BasicNode node) {
1046    if (node == rootNode) {
1047      return LDAPConnectionPool.makeLDAPUrl(ctxConfiguration, "");
1048    }
1049    final BasicNode parent = (BasicNode) node.getParent();
1050    if (parent != null)
1051    {
1052      final LDAPURL parentUrl = findUrlForDisplayedEntry(parent);
1053      return LDAPConnectionPool.makeLDAPUrl(parentUrl, node.getDN());
1054    }
1055    return LDAPConnectionPool.makeLDAPUrl(ctxConfiguration, node.getDN());
1056  }
1057
1058
1059  /**
1060   * Returns the displayed entry URL for a given node.
1061   * @param node the node.
1062   * @return the displayed entry URL for a given node.
1063   */
1064  private LDAPURL findUrlForDisplayedEntry(BasicNode node)
1065  {
1066    if (followReferrals && node.getRemoteUrl() != null) {
1067      return node.getRemoteUrl();
1068    }
1069    return findUrlForLocalEntry(node);
1070  }
1071
1072
1073  /**
1074   * Returns the DN to use for searching children of a given node.
1075   * In most cases, it's node.getDN(). However if node has referral data
1076   * and _followReferrals is true, the result is calculated from the
1077   * referral resolution.
1078   *
1079   * @param node the node.
1080   * @return the DN to use for searching children of a given node.
1081   */
1082  String findBaseDNForChildEntries(BasicNode node) {
1083    if (followReferrals && node.getRemoteUrl() != null) {
1084      return node.getRemoteUrl().getRawBaseDN();
1085    }
1086    return node.getDN();
1087  }
1088
1089
1090
1091  /**
1092   * Tells whether a node is displaying a remote entry.
1093   * @param node the node.
1094   * @return <CODE>true</CODE> if the node displays a remote entry and
1095   * <CODE>false</CODE> otherwise.
1096   */
1097  private boolean isDisplayedEntryRemote(BasicNode node) {
1098    if (followReferrals) {
1099      if (node == rootNode) {
1100        return false;
1101      }
1102      if (node.getRemoteUrl() != null) {
1103        return true;
1104      }
1105      final BasicNode parent = (BasicNode)node.getParent();
1106      if (parent != null) {
1107        return isDisplayedEntryRemote(parent);
1108      }
1109    }
1110    return false;
1111  }
1112
1113
1114  /**
1115   * Returns the list of attributes for the red search.
1116   * @return the list of attributes for the red search.
1117   */
1118  String[] getAttrsForRedSearch() {
1119    ArrayList<String> v = new ArrayList<>();
1120
1121    v.add(OBJECTCLASS_ATTRIBUTE_TYPE_NAME);
1122    v.add(NUMSUBORDINATES_ATTR);
1123    v.add(HASSUBORDINATES_ATTR);
1124    v.add(ATTR_REFERRAL_URL);
1125    if ((displayFlags & DISPLAY_ACI_COUNT) != 0) {
1126      v.add(ACI_ATTR);
1127    }
1128    if (!RDN_ATTRIBUTE.equals(displayAttribute)) {
1129      v.add(displayAttribute);
1130    }
1131
1132    return v.toArray(new String[v.size()]);
1133  }
1134
1135  /**
1136   * Returns the list of attributes for the black search.
1137   * @return the list of attributes for the black search.
1138   */
1139  String[] getAttrsForBlackSearch() {
1140    if (!RDN_ATTRIBUTE.equals(displayAttribute)) {
1141      return new String[] {
1142          OBJECTCLASS_ATTRIBUTE_TYPE_NAME,
1143          NUMSUBORDINATES_ATTR,
1144          HASSUBORDINATES_ATTR,
1145          ATTR_REFERRAL_URL,
1146          ACI_ATTR,
1147          displayAttribute};
1148    } else {
1149      return new String[] {
1150          OBJECTCLASS_ATTRIBUTE_TYPE_NAME,
1151          NUMSUBORDINATES_ATTR,
1152          HASSUBORDINATES_ATTR,
1153          ATTR_REFERRAL_URL,
1154          ACI_ATTR
1155      };
1156    }
1157  }
1158
1159  /**
1160   * Returns the basic search controls.
1161   * @return the basic search controls.
1162   */
1163  SearchControls getBasicSearchControls() {
1164    SearchControls searchControls = new SearchControls();
1165    searchControls.setCountLimit(maxChildren);
1166    return searchControls;
1167  }
1168
1169  /**
1170   * Returns the request controls to search user data.
1171   * @return the request controls to search user data.
1172   */
1173  private Control[] getRequestControls()
1174  {
1175    Control ctls[];
1176    if (followReferrals)
1177    {
1178      ctls = new Control[sorted ? 2 : 1];
1179    }
1180    else
1181    {
1182      ctls = new Control[sorted ? 1 : 0];
1183    }
1184    if (sorted)
1185    {
1186      SortKey[] keys = new SortKey[SORT_ATTRIBUTES.length];
1187      for (int i=0; i<keys.length; i++) {
1188        keys[i] = new SortKey(SORT_ATTRIBUTES[i]);
1189      }
1190      try
1191      {
1192        ctls[0] = new SortControl(keys, false);
1193      }
1194      catch (IOException ioe)
1195      {
1196        // Bug
1197        throw new RuntimeException("Unexpected encoding exception: "+ioe,
1198            ioe);
1199      }
1200    }
1201    if (followReferrals)
1202    {
1203      ctls[ctls.length - 1] = new ManageReferralControl(false);
1204    }
1205    return ctls;
1206  }
1207
1208  /**
1209   * Returns the request controls to search configuration data.
1210   * @return the request controls to search configuration data.
1211   */
1212  private Control[] getConfigurationRequestControls()
1213  {
1214    return getRequestControls();
1215  }
1216
1217
1218  /**
1219   * Callbacks invoked by task classes
1220   * =================================
1221   *
1222   * The routines below are invoked by the task classes; they
1223   * update the nodes and the tree model.
1224   *
1225   * To ensure the consistency of the tree model, these routines
1226   * are not invoked directly by the task classes: they are
1227   * invoked using SwingUtilities.invokeAndWait() (each of the
1228   * methods XXX() below has a matching wrapper invokeXXX()).
1229   *
1230   */
1231
1232  /**
1233   * Invoked when the refresh task has finished the red operation.
1234   * It has read the attributes of the base entry ; the result of the
1235   * operation is:
1236   *    - an LDAPEntry if successful
1237   *    - an Exception if failed
1238   * @param task the task that progressed.
1239   * @param oldState the previous state of the task.
1240   * @param newState the new state of the task.
1241   * @throws NamingException if there is an error reading entries.
1242   */
1243  private void refreshTaskDidProgress(NodeRefresher task,
1244      NodeRefresher.State oldState,
1245      NodeRefresher.State newState) throws NamingException {
1246    BasicNode node = task.getNode();
1247    boolean nodeChanged = false;
1248
1249    //task.dump();
1250
1251    // Manage events
1252    if (oldState == NodeRefresher.State.QUEUED) {
1253      checkUpdateEvent(true);
1254    }
1255    if (task.isInFinalState()) {
1256      checkUpdateEvent(false);
1257    }
1258
1259    if (newState == NodeRefresher.State.FAILED) {
1260      // In case of NameNotFoundException, we simply remove the node from the
1261      // tree.
1262      // Except when it's due a to referral resolution: we keep the node
1263      // in order the user can fix the referral.
1264      if (isNameNotFoundException(task.getException())
1265          && oldState != NodeRefresher.State.SOLVING_REFERRAL) {
1266        removeOneNode(node);
1267      }
1268      else {
1269        if (oldState == NodeRefresher.State.SOLVING_REFERRAL)
1270        {
1271          node.setRemoteUrl(task.getRemoteUrl());
1272          if (task.getRemoteEntry() != null)
1273          {
1274            /* This is the case when there are multiple hops in the referral
1275           and so we have a remote referral entry but not the entry that it
1276           points to */
1277            updateNodeRendering(node, task.getRemoteEntry());
1278          }
1279          /* It is a referral and we try to follow referrals.
1280         We remove its children (that are supposed to be
1281         entries on the remote server).
1282         If this referral entry has children locally (even if this goes
1283         against the recommendation of the standards) these children will
1284         NOT be displayed. */
1285
1286          node.setLeaf(true);
1287          removeAllChildNodes(node, true /* Keep suffixes */);
1288        }
1289        node.setError(new BasicNodeError(oldState, task.getException(),
1290            task.getExceptionArg()));
1291        nodeChanged = updateNodeRendering(node, task.getDisplayedEntry());
1292      }
1293    }
1294    else if (newState == NodeRefresher.State.CANCELLED ||
1295        newState == NodeRefresher.State.INTERRUPTED) {
1296
1297      // Let's collapse task.getNode()
1298      tree.collapsePath(new TreePath(treeModel.getPathToRoot(node)));
1299
1300      // TODO: should we reflect this situation visually ?
1301    }
1302    else {
1303
1304      if (oldState != NodeRefresher.State.SEARCHING_CHILDREN
1305          && newState == NodeRefresher.State.SEARCHING_CHILDREN) {
1306        // The children search is going to start
1307        if (canDoDifferentialUpdate(task)) {
1308          Enumeration<?> e = node.children();
1309          while (e.hasMoreElements()) {
1310            BasicNode child = (BasicNode)e.nextElement();
1311            child.setObsolete(true);
1312          }
1313        }
1314        else {
1315          removeAllChildNodes(node, true /* Keep suffixes */);
1316        }
1317      }
1318
1319      if (oldState == NodeRefresher.State.READING_LOCAL_ENTRY) {
1320        /* The task is going to try to solve the referral if there's one.
1321         If succeeds we will update the remote url.  Set it to null for
1322         the case when there was a referral and it has been deleted */
1323        node.setRemoteUrl((String)null);
1324        SearchResult localEntry = task.getLocalEntry();
1325        nodeChanged = updateNodeRendering(node, localEntry);
1326      }
1327      else if (oldState == NodeRefresher.State.SOLVING_REFERRAL) {
1328        node.setRemoteUrl(task.getRemoteUrl());
1329        updateNodeRendering(node, task.getRemoteEntry());
1330        nodeChanged = true;
1331      }
1332      else if (oldState == NodeRefresher.State.DETECTING_CHILDREN) {
1333        if (node.isLeaf() != task.isLeafNode()) {
1334          node.setLeaf(task.isLeafNode());
1335          updateNodeRendering(node, task.getDisplayedEntry());
1336          nodeChanged = true;
1337          if (node.isLeaf()) {
1338            /* We didn't detect any child: remove the previously existing
1339             * ones */
1340            removeAllChildNodes(node, false /* Remove suffixes */);
1341          }
1342        }
1343      }
1344      else if (oldState == NodeRefresher.State.SEARCHING_CHILDREN) {
1345
1346        updateChildNodes(task);
1347        if (newState == NodeRefresher.State.FINISHED) {
1348          // The children search is finished
1349          if (canDoDifferentialUpdate(task)) {
1350            // Remove obsolete child nodes
1351            // Note: we scan in the reverse order to preserve indexes
1352            for (int i = node.getChildCount()-1; i >= 0; i--) {
1353              BasicNode child = (BasicNode)node.getChildAt(i);
1354              if (child.isObsolete()) {
1355                removeOneNode(child);
1356              }
1357            }
1358          }
1359          // The node may have become a leaf.
1360          if (node.getChildCount() == 0) {
1361            node.setLeaf(true);
1362            updateNodeRendering(node, task.getDisplayedEntry());
1363            nodeChanged = true;
1364          }
1365        }
1366        if (node.isSizeLimitReached())
1367        {
1368          fireEvent(BrowserEvent.Type.SIZE_LIMIT_REACHED);
1369        }
1370      }
1371
1372      if (newState == NodeRefresher.State.FINISHED && node.getError() != null) {
1373        node.setError(null);
1374        nodeChanged = updateNodeRendering(node, task.getDisplayedEntry());
1375      }
1376    }
1377
1378
1379    if (nodeChanged) {
1380      treeModel.nodeChanged(task.getNode());
1381    }
1382
1383    if (node.isLeaf() && node.getChildCount() >= 1) {
1384      throw new RuntimeException("Inconsistent node: " + node.getDN());
1385    }
1386  }
1387
1388
1389  /**
1390   * Commodity method that calls the method refreshTaskDidProgress in the event
1391   * thread.
1392   * @param task the task that progressed.
1393   * @param oldState the previous state of the task.
1394   * @param newState the new state of the task.
1395   * @throws InterruptedException if an errors occurs invoking the method.
1396   */
1397  void invokeRefreshTaskDidProgress(final NodeRefresher task,
1398      final NodeRefresher.State oldState,
1399      final NodeRefresher.State newState)
1400  throws InterruptedException {
1401    Runnable r = new Runnable() {
1402      @Override
1403      public void run() {
1404        try {
1405          refreshTaskDidProgress(task, oldState, newState);
1406        }
1407        catch(Throwable t)
1408        {
1409          LOG.log(Level.SEVERE, "Error calling refreshTaskDidProgress: "+t, t);
1410        }
1411      }
1412    };
1413    swingInvoke(r);
1414  }
1415
1416
1417
1418  /**
1419   * Core routines shared by the callbacks above
1420   * ===========================================
1421   */
1422
1423  /**
1424   * Updates the child nodes for a given task.
1425   * @param task the task.
1426   * @throws NamingException if an error occurs.
1427   */
1428  private void updateChildNodes(NodeRefresher task) throws NamingException {
1429    BasicNode parent = task.getNode();
1430    ArrayList<Integer> insertIndex = new ArrayList<>();
1431    ArrayList<Integer> changedIndex = new ArrayList<>();
1432    boolean differential = canDoDifferentialUpdate(task);
1433
1434    // NUMSUBORDINATE HACK
1435    // To avoid testing each child to the hacker,
1436    // we verify here if the parent node is parent of
1437    // any entry listed in the hacker.
1438    // In most case, the doNotTrust flag will false and
1439    // no overhead will be caused in the child loop.
1440    LDAPURL parentUrl = findUrlForDisplayedEntry(parent);
1441    boolean doNotTrust = numSubordinateHacker.containsChildrenOf(parentUrl);
1442
1443    // Walk through the entries
1444    for (SearchResult entry : task.getChildEntries())
1445    {
1446      BasicNode child;
1447
1448      // Search a child node matching the DN of the entry
1449      int index;
1450      if (differential) {
1451//      System.out.println("Differential mode -> starting to search");
1452        index = findChildNode(parent, entry.getName());
1453//      System.out.println("Differential mode -> ending to search");
1454      }
1455      else {
1456        index = - (parent.getChildCount() + 1);
1457      }
1458
1459      // If no node matches, we create a new node
1460      if (index < 0) {
1461        // -(index + 1) is the location where to insert the new node
1462        index = -(index + 1);
1463        child = new BasicNode(entry.getName());
1464        parent.insert(child, index);
1465        updateNodeRendering(child, entry);
1466        insertIndex.add(index);
1467//      System.out.println("Inserted " + child.getDN() + " at " + index);
1468      }
1469      else { // Else we update the existing one
1470        child = (BasicNode)parent.getChildAt(index);
1471        if (updateNodeRendering(child, entry)) {
1472          changedIndex.add(index);
1473        }
1474        // The node is no longer obsolete
1475        child.setObsolete(false);
1476      }
1477
1478      // NUMSUBORDINATE HACK
1479      // Let's see if child has subordinates or not.
1480      // Thanks to slapd, we cannot always trust the numSubOrdinates attribute.
1481      // If the child entry's DN is found in the hacker's list, then we ignore
1482      // the numSubordinate attribute... :((
1483      boolean hasNoSubOrdinates;
1484      if (!child.hasSubOrdinates() && doNotTrust) {
1485        hasNoSubOrdinates = !numSubordinateHacker.contains(
1486            findUrlForDisplayedEntry(child));
1487      }
1488      else {
1489        hasNoSubOrdinates = !child.hasSubOrdinates();
1490      }
1491
1492
1493
1494      // Propagate the refresh
1495      // Note: logically we should unconditionally call:
1496      //  startRefreshNode(child, false, true);
1497      //
1498      // However doing that saturates refreshQueue
1499      // with many nodes. And, by design, RefreshTask
1500      // won't do anything on a node if:
1501      //    - this node has no subordinates
1502      //    - *and* this node has no referral data
1503      // So we test these conditions here and
1504      // skip the call to startRefreshNode() if
1505      // possible.
1506      //
1507      // The exception to this is the case where the
1508      // node had children (in the tree).  In this case
1509      // we force the refresh. See bug 5015115
1510      //
1511      if (!hasNoSubOrdinates
1512          || child.getReferral() != null
1513          || child.getChildCount() > 0) {
1514        startRefreshNode(child, entry, true);
1515      }
1516    }
1517
1518
1519    // Inform the tree model that we have created some new nodes
1520    if (insertIndex.size() >= 1) {
1521      treeModel.nodesWereInserted(parent, intArrayFromCollection(insertIndex));
1522    }
1523    if (changedIndex.size() >= 1) {
1524      treeModel.nodesChanged(parent, intArrayFromCollection(changedIndex));
1525    }
1526  }
1527
1528
1529
1530  /**
1531   * Tells whether a differential update can be made in the provided task.
1532   * @param task the task.
1533   * @return <CODE>true</CODE> if a differential update can be made and
1534   * <CODE>false</CODE> otherwise.
1535   */
1536  private boolean canDoDifferentialUpdate(NodeRefresher task) {
1537    return task.getNode().getChildCount() >= 1
1538        && task.getNode().getNumSubOrdinates() <= 100;
1539  }
1540
1541
1542  /**
1543   * Recompute the rendering props of a node (text, style, icon) depending on.
1544   *    - the state of this node
1545   *    - the LDAPEntry displayed by this node
1546   * @param node the node to be rendered.
1547   * @param entry the search result for the entry that the node represents.
1548   */
1549  private boolean updateNodeRendering(BasicNode node, SearchResult entry)
1550  throws NamingException {
1551    if (entry != null) {
1552      node.setNumSubOrdinates(getNumSubOrdinates(entry));
1553      node.setHasSubOrdinates(
1554          node.getNumSubOrdinates() > 0 || getHasSubOrdinates(entry));
1555      node.setReferral(getReferral(entry));
1556      Set<String> ocValues = ConnectionUtils.getValues(entry,
1557          OBJECTCLASS_ATTRIBUTE_TYPE_NAME);
1558      if (ocValues != null) {
1559        node.setObjectClassValues(ocValues.toArray(new String[ocValues.size()]));
1560      }
1561    }
1562
1563    int aciCount = getAciCount(entry);
1564    Icon newIcon = getNewIcon(node, entry);
1565
1566    // Construct the icon text according the dn, the aci count...
1567    StringBuilder sb2 = new StringBuilder();
1568    if (aciCount >= 1) {
1569      sb2.append(aciCount);
1570      sb2.append(" aci");
1571      if (aciCount != 1) {
1572        sb2.append("s");
1573      }
1574    }
1575
1576    StringBuilder sb1 = new StringBuilder();
1577    if (node instanceof SuffixNode) {
1578      if (entry != null) {
1579        sb1.append(entry.getName());
1580      }
1581    } else {
1582      boolean useRdn = true;
1583      if (!RDN_ATTRIBUTE.equals(displayAttribute) && entry != null) {
1584        String value = ConnectionUtils.getFirstValue(entry,displayAttribute);
1585        if (value != null) {
1586          if (showAttributeName) {
1587            value = displayAttribute+"="+value;
1588          }
1589          sb1.append(value);
1590          useRdn = false;
1591        }
1592      }
1593
1594      if (useRdn) {
1595        String rdn;
1596        if (followReferrals && node.getRemoteUrl() != null) {
1597          if (showAttributeName) {
1598            rdn = node.getRemoteRDNWithAttributeName();
1599          } else {
1600            rdn = node.getRemoteRDN();
1601          }
1602        }
1603        else {
1604          if (showAttributeName) {
1605            rdn = node.getRDNWithAttributeName();
1606          } else {
1607            rdn = node.getRDN();
1608          }
1609        }
1610        sb1.append(rdn);
1611      }
1612    }
1613    if (sb2.length() >= 1) {
1614      sb1.append("  (");
1615      sb1.append(sb2);
1616      sb1.append(")");
1617    }
1618    String newDisplayName = sb1.toString();
1619
1620    // Select the font style according referral
1621    int newStyle = 0;
1622    if (isDisplayedEntryRemote(node)) {
1623      newStyle |= Font.ITALIC;
1624    }
1625
1626    // Determine if the rendering needs to be updated
1627    boolean changed =
1628        node.getIcon() != newIcon
1629        || !node.getDisplayName().equals(newDisplayName)
1630        || node.getFontStyle() != newStyle;
1631    if (changed) {
1632      node.setIcon(newIcon);
1633      node.setDisplayName(newDisplayName);
1634      node.setFontStyle(newStyle);
1635    }
1636    return changed;
1637  }
1638
1639  private int getAciCount(SearchResult entry) throws NamingException
1640  {
1641    if ((displayFlags & DISPLAY_ACI_COUNT) != 0 && entry != null) {
1642      Set<String> aciValues = ConnectionUtils.getValues(entry, "aci");
1643      if (aciValues != null) {
1644        return aciValues.size();
1645      }
1646    }
1647    return 0;
1648  }
1649
1650
1651  private Icon getNewIcon(BasicNode node, SearchResult entry)
1652      throws NamingException
1653  {
1654    // Select the icon according the objectClass,...
1655    int modifiers = 0;
1656    if (node.isLeaf() && !node.hasSubOrdinates()) {
1657      modifiers |= IconPool.MODIFIER_LEAF;
1658    }
1659    if (node.getReferral() != null) {
1660      modifiers |= IconPool.MODIFIER_REFERRAL;
1661    }
1662    if (node.getError() != null) {
1663      final Exception ex = node.getError().getException();
1664      if (ex != null)
1665      {
1666        LOG.log(Level.SEVERE, "node has error: " + ex, ex);
1667      }
1668      modifiers |= IconPool.MODIFIER_ERROR;
1669    }
1670
1671    SortedSet<String> objectClasses = new TreeSet<>();
1672    if (entry != null) {
1673      Set<String> ocs = ConnectionUtils.getValues(entry, "objectClass");
1674      if (ocs != null)
1675      {
1676        objectClasses.addAll(ocs);
1677      }
1678    }
1679
1680    if (node instanceof SuffixNode)
1681    {
1682      return iconPool.getSuffixIcon();
1683    }
1684    return iconPool.getIcon(objectClasses, modifiers);
1685  }
1686
1687  /**
1688   * Find a child node matching a given DN.
1689   *
1690   * result >= 0    result is the index of the node matching childDn.
1691   * result < 0   -(result + 1) is the index at which the new node must be
1692   * inserted.
1693   * @param parent the parent node of the node that is being searched.
1694   * @param childDn the DN of the entry that is being searched.
1695   * @return the index of the node matching childDn.
1696   */
1697  public int findChildNode(BasicNode parent, String childDn) {
1698    int childCount = parent.getChildCount();
1699    int i = 0;
1700    while (i < childCount
1701        && !childDn.equals(((BasicNode)parent.getChildAt(i)).getDN())) {
1702      i++;
1703    }
1704    if (i >= childCount) { // Not found
1705      i = -(childCount + 1);
1706    }
1707    return i;
1708  }
1709
1710  /**
1711   * Remove a single node from the tree model.
1712   * It takes care to cancel all the tasks associated to this node.
1713   * @param node the node to be removed.
1714   */
1715  private void removeOneNode(BasicNode node) {
1716    stopRefreshNode(node);
1717    treeModel.removeNodeFromParent(node);
1718  }
1719
1720
1721  /**
1722   * BrowserEvent management
1723   * =======================
1724   *
1725   * This method computes the total size of the queues,
1726   * compares this value with the last computed and
1727   * decides if an update event should be fired or not.
1728   *
1729   * It's invoked by task classes through SwingUtilities.invokeLater()
1730   * (see the wrapper below). That means the event handling routine
1731   * (processBrowserEvent) is executed in the event thread.
1732   * @param taskIsStarting whether the task is starting or not.
1733   */
1734  private void checkUpdateEvent(boolean taskIsStarting) {
1735    int newSize = refreshQueue.size();
1736    if (!taskIsStarting) {
1737      newSize = newSize - 1;
1738    }
1739    if (newSize != queueTotalSize) {
1740      if (queueTotalSize == 0 && newSize >= 1) {
1741        fireEvent(BrowserEvent.Type.UPDATE_START);
1742      }
1743      else if (queueTotalSize >= 1 && newSize == 0) {
1744        fireEvent(BrowserEvent.Type.UPDATE_END);
1745      }
1746      queueTotalSize = newSize;
1747    }
1748  }
1749
1750  /**
1751   * Returns the size of the queue containing the different tasks.  It can be
1752   * used to know if there are search operations ongoing.
1753   * @return the number of RefreshTask operations ongoing (or waiting to start).
1754   */
1755  public int getQueueSize()
1756  {
1757    return refreshQueue.size();
1758  }
1759
1760
1761  /**
1762   * Fires a BrowserEvent.
1763   * @param type the type of the event.
1764   */
1765  private void fireEvent(BrowserEvent.Type type) {
1766    BrowserEvent event = new BrowserEvent(this, type);
1767    for (BrowserEventListener listener : listeners)
1768    {
1769      listener.processBrowserEvent(event);
1770    }
1771  }
1772
1773
1774  /**
1775   * Miscellaneous private routines
1776   * ==============================
1777   */
1778
1779
1780  /**
1781   * Find a SuffixNode in the tree model.
1782   * @param suffixDn the dn of the suffix node.
1783   * @param suffixNode the node from which we start searching.
1784   * @return the SuffixNode associated with the provided DN.  <CODE>null</CODE>
1785   * if nothing is found.
1786   * @throws IllegalArgumentException if a node with the given dn exists but
1787   * is not a suffix node.
1788   */
1789  private SuffixNode findSuffixNode(String suffixDn, SuffixNode suffixNode)
1790      throws IllegalArgumentException
1791  {
1792    if (Utilities.areDnsEqual(suffixNode.getDN(), suffixDn)) {
1793      return suffixNode;
1794    }
1795
1796    int childCount = suffixNode.getChildCount();
1797    if (childCount == 0)
1798    {
1799      return null;
1800    }
1801    BasicNode child;
1802    int i = 0;
1803    boolean found = false;
1804    do
1805    {
1806      child = (BasicNode) suffixNode.getChildAt(i);
1807      if (Utilities.areDnsEqual(child.getDN(), suffixDn))
1808      {
1809        found = true;
1810      }
1811      i++;
1812    }
1813    while (i < childCount && !found);
1814
1815    if (!found)
1816    {
1817      return null;
1818    }
1819    if (child instanceof SuffixNode)
1820    {
1821      return (SuffixNode) child;
1822    }
1823
1824    // A node matches suffixDn however it's not a suffix node.
1825    // There's a bug in the caller.
1826    throw new IllegalArgumentException(suffixDn + " is not a suffix node");
1827  }
1828
1829
1830
1831  /**
1832   * Return <CODE>true</CODE> if x is a non <code>null</code>
1833   * NameNotFoundException.
1834   * @return <CODE>true</CODE> if x is a non <code>null</code>
1835   * NameNotFoundException.
1836   */
1837  private boolean isNameNotFoundException(Object x) {
1838    return x instanceof NameNotFoundException;
1839  }
1840
1841
1842
1843  /**
1844   * Get the value of the numSubordinates attribute.
1845   * If numSubordinates is not present, returns 0.
1846   * @param entry the entry to analyze.
1847   * @throws NamingException if an error occurs.
1848   * @return the value of the numSubordinates attribute.  0 if the attribute
1849   * could not be found.
1850   */
1851  private static int getNumSubOrdinates(SearchResult entry) throws NamingException
1852  {
1853    return toInt(ConnectionUtils.getFirstValue(entry, NUMSUBORDINATES_ATTR));
1854  }
1855
1856  /**
1857   * Returns whether the entry has subordinates or not.  It uses an algorithm
1858   * based in hasSubordinates and numSubordinates attributes.
1859   * @param entry the entry to analyze.
1860   * @throws NamingException if an error occurs.
1861   * @return {@code true} if the entry has subordinates according to the values
1862   * of hasSubordinates and numSubordinates, returns {@code false} if none of
1863   * the attributes could be found.
1864   */
1865  public static boolean getHasSubOrdinates(SearchResult entry)
1866  throws NamingException
1867  {
1868    String v = ConnectionUtils.getFirstValue(entry, HASSUBORDINATES_ATTR);
1869    if (v != null) {
1870      return "true".equalsIgnoreCase(v);
1871    }
1872    return getNumSubOrdinates(entry) > 0;
1873  }
1874
1875  /**
1876   * Get the value of the numSubordinates attribute.
1877   * If numSubordinates is not present, returns 0.
1878   * @param entry the entry to analyze.
1879   * @return the value of the numSubordinates attribute.  0 if the attribute
1880   * could not be found.
1881   */
1882  private static int getNumSubOrdinates(CustomSearchResult entry)
1883  {
1884    List<Object> vs = entry.getAttributeValues(NUMSUBORDINATES_ATTR);
1885    String v = null;
1886    if (vs != null && !vs.isEmpty())
1887    {
1888      v = vs.get(0).toString();
1889    }
1890    return toInt(v);
1891  }
1892
1893
1894  private static int toInt(String v)
1895  {
1896    if (v == null)
1897    {
1898      return 0;
1899    }
1900    try
1901    {
1902      return Integer.parseInt(v);
1903    }
1904    catch (NumberFormatException x)
1905    {
1906      return 0;
1907    }
1908  }
1909
1910  /**
1911   * Returns whether the entry has subordinates or not.  It uses an algorithm
1912   * based in hasSubordinates and numSubordinates attributes.
1913   * @param entry the entry to analyze.
1914   * @return {@code true} if the entry has subordinates according to the values
1915   * of hasSubordinates and numSubordinates, returns {@code false} if none of
1916   * the attributes could be found.
1917   */
1918  public static boolean getHasSubOrdinates(CustomSearchResult entry)
1919  {
1920    List<Object> vs = entry.getAttributeValues(HASSUBORDINATES_ATTR);
1921    String v = null;
1922    if (vs != null && !vs.isEmpty())
1923    {
1924      v = vs.get(0).toString();
1925    }
1926    if (v != null)
1927    {
1928      return "true".equalsIgnoreCase(v);
1929    }
1930    return getNumSubOrdinates(entry) > 0;
1931  }
1932
1933
1934  /**
1935   * Returns the value of the 'ref' attribute.
1936   * <CODE>null</CODE> if the attribute is not present.
1937   * @param entry the entry to analyze.
1938   * @throws NamingException if an error occurs.
1939   * @return the value of the ref attribute.  <CODE>null</CODE> if the attribute
1940   * could not be found.
1941   */
1942  public static String[] getReferral(SearchResult entry) throws NamingException
1943  {
1944    String[] result = null;
1945    Set<String> values = ConnectionUtils.getValues(entry,
1946        OBJECTCLASS_ATTRIBUTE_TYPE_NAME);
1947    if (values != null)
1948    {
1949      for (String value : values)
1950      {
1951        boolean isReferral = "referral".equalsIgnoreCase(value);
1952        if (isReferral)
1953        {
1954          Set<String> refValues = ConnectionUtils.getValues(entry,
1955              ATTR_REFERRAL_URL);
1956          if (refValues != null)
1957          {
1958            result = new String[refValues.size()];
1959            refValues.toArray(result);
1960          }
1961          break;
1962        }
1963      }
1964    }
1965    return result;
1966  }
1967
1968
1969  /**
1970   * Returns true if the node is expanded.
1971   * @param node the node to analyze.
1972   * @return <CODE>true</CODE> if the node is expanded and <CODE>false</CODE>
1973   * otherwise.
1974   */
1975  public boolean nodeIsExpanded(BasicNode node) {
1976    TreePath tp = new TreePath(treeModel.getPathToRoot(node));
1977    return tree.isExpanded(tp);
1978  }
1979
1980  /**
1981   * Expands node. Must be run from the event thread.  This is called
1982   * when the node is automatically expanded.
1983   * @param node the node to expand.
1984   */
1985  public void expandNode(BasicNode node) {
1986    automaticallyExpandedNode = true;
1987    TreePath tp = new TreePath(treeModel.getPathToRoot(node));
1988    tree.expandPath(tp);
1989    tree.fireTreeExpanded(tp);
1990    automaticallyExpandedNode = false;
1991  }
1992
1993
1994
1995  /**
1996   * Collection utilities
1997   */
1998  /**
1999   * Returns an array of integer from a Collection of Integer objects.
2000   * @param v the Collection of Integer objects.
2001   * @return an array of int from a Collection of Integer objects.
2002   */
2003  private static int[] intArrayFromCollection(Collection<Integer> v) {
2004    int[] result = new int[v.size()];
2005    int i = 0;
2006    for (Integer value : v)
2007    {
2008      result[i] = value;
2009      i++;
2010    }
2011    return result;
2012  }
2013
2014
2015  /**
2016   * For debugging purpose: allows to switch easily
2017   * between invokeLater() and invokeAndWait() for
2018   * experimentation...
2019   * @param r the runnable to be invoked.
2020   * @throws InterruptedException if there is an error invoking SwingUtilities.
2021   */
2022  private static void swingInvoke(Runnable r) throws InterruptedException {
2023    try {
2024      SwingUtilities.invokeAndWait(r);
2025    }
2026    catch(InterruptedException x) {
2027      throw x;
2028    }
2029    catch(InvocationTargetException x) {
2030      // Probably a very big trouble...
2031      x.printStackTrace();
2032    }
2033  }
2034
2035
2036  /**
2037   * The default implementation of the BrowserNodeInfo interface.
2038   */
2039  private class BrowserNodeInfoImpl implements BrowserNodeInfo
2040  {
2041    private BasicNode node;
2042    private LDAPURL url;
2043    private boolean isRemote;
2044    private boolean isSuffix;
2045    private boolean isRootNode;
2046    private String[] referral;
2047    private int numSubOrdinates;
2048    private boolean hasSubOrdinates;
2049    private int errorType;
2050    private Exception errorException;
2051    private Object errorArg;
2052    private String[] objectClassValues;
2053    private String toString;
2054
2055    /**
2056     * The constructor of this object.
2057     * @param node the node in the tree that is used.
2058     */
2059    public BrowserNodeInfoImpl(BasicNode node) {
2060      this.node = node;
2061      url = findUrlForDisplayedEntry(node);
2062
2063      isRootNode = node instanceof RootNode;
2064      isRemote = isDisplayedEntryRemote(node);
2065      isSuffix = node instanceof SuffixNode;
2066      referral = node.getReferral();
2067      numSubOrdinates = node.getNumSubOrdinates();
2068      hasSubOrdinates = node.hasSubOrdinates();
2069      objectClassValues = node.getObjectClassValues();
2070      if (node.getError() != null) {
2071        BasicNodeError error = node.getError();
2072        switch(error.getState()) {
2073        case READING_LOCAL_ENTRY:
2074          errorType = ERROR_READING_ENTRY;
2075          break;
2076        case SOLVING_REFERRAL:
2077          errorType = ERROR_SOLVING_REFERRAL;
2078          break;
2079        case DETECTING_CHILDREN:
2080        case SEARCHING_CHILDREN:
2081          errorType = ERROR_SEARCHING_CHILDREN;
2082          break;
2083
2084        }
2085        errorException = error.getException();
2086        errorArg = error.getArg();
2087      }
2088      StringBuilder sb = new StringBuilder();
2089      sb.append(getURL());
2090      if (getReferral() != null) {
2091        sb.append(" -> ");
2092        sb.append(getReferral());
2093      }
2094      toString = sb.toString();
2095    }
2096
2097    /**
2098     * Returns the node associated with this object.
2099     * @return  the node associated with this object.
2100     */
2101    @Override
2102    public BasicNode getNode() {
2103      return node;
2104    }
2105
2106    /**
2107     * Returns the LDAP URL associated with this object.
2108     * @return the LDAP URL associated with this object.
2109     */
2110    @Override
2111    public LDAPURL getURL() {
2112      return url;
2113    }
2114
2115    /**
2116     * Tells whether this is a root node or not.
2117     * @return <CODE>true</CODE> if this is a root node and <CODE>false</CODE>
2118     * otherwise.
2119     */
2120    @Override
2121    public boolean isRootNode() {
2122      return isRootNode;
2123    }
2124
2125    /**
2126     * Tells whether this is a suffix node or not.
2127     * @return <CODE>true</CODE> if this is a suffix node and <CODE>false</CODE>
2128     * otherwise.
2129     */
2130    @Override
2131    public boolean isSuffix() {
2132      return isSuffix;
2133    }
2134
2135    /**
2136     * Tells whether this is a remote node or not.
2137     * @return <CODE>true</CODE> if this is a remote node and <CODE>false</CODE>
2138     * otherwise.
2139     */
2140    @Override
2141    public boolean isRemote() {
2142      return isRemote;
2143    }
2144
2145    /**
2146     * Returns the list of referral associated with this node.
2147     * @return the list of referral associated with this node.
2148     */
2149    @Override
2150    public String[] getReferral() {
2151      return referral;
2152    }
2153
2154    /**
2155     * Returns the number of subordinates of the entry associated with this
2156     * node.
2157     * @return the number of subordinates of the entry associated with this
2158     * node.
2159     */
2160    @Override
2161    public int getNumSubOrdinates() {
2162      return numSubOrdinates;
2163    }
2164
2165    /**
2166     * Returns whether the entry has subordinates or not.
2167     * @return {@code true} if the entry has subordinates and {@code false}
2168     * otherwise.
2169     */
2170    @Override
2171    public boolean hasSubOrdinates() {
2172      return hasSubOrdinates;
2173    }
2174
2175    /**
2176     * Returns the error type associated we got when refreshing the node.
2177     * <CODE>null</CODE> if no error was found.
2178     * @return the error type associated we got when refreshing the node.
2179     * <CODE>null</CODE> if no error was found.
2180     */
2181    @Override
2182    public int getErrorType() {
2183      return errorType;
2184    }
2185
2186    /**
2187     * Returns the exception associated we got when refreshing the node.
2188     * <CODE>null</CODE> if no exception was found.
2189     * @return the exception associated we got when refreshing the node.
2190     * <CODE>null</CODE> if no exception was found.
2191     */
2192    @Override
2193    public Exception getErrorException() {
2194      return errorException;
2195    }
2196
2197    /**
2198     * Returns the error argument associated we got when refreshing the node.
2199     * <CODE>null</CODE> if no error argument was found.
2200     * @return the error argument associated we got when refreshing the node.
2201     * <CODE>null</CODE> if no error argument was found.
2202     */
2203    @Override
2204    public Object getErrorArg() {
2205      return errorArg;
2206    }
2207
2208    /**
2209     * Return the tree path associated with the node in the tree.
2210     * @return the tree path associated with the node in the tree.
2211     */
2212    @Override
2213    public TreePath getTreePath() {
2214      return new TreePath(treeModel.getPathToRoot(node));
2215    }
2216
2217    /**
2218     * Returns the object class values of the entry associated with the node.
2219     * @return the object class values of the entry associated with the node.
2220     */
2221    @Override
2222    public String[] getObjectClassValues() {
2223      return objectClassValues;
2224    }
2225
2226    /**
2227     * Returns a String representation of the object.
2228     * @return a String representation of the object.
2229     */
2230    @Override
2231    public String toString() {
2232      return toString;
2233    }
2234
2235    /**
2236     * Compares the provide node with this object.
2237     * @param node the node.
2238     * @return <CODE>true</CODE> if the node info represents the same node as
2239     * this and <CODE>false</CODE> otherwise.
2240     */
2241    @Override
2242    public boolean representsSameNode(BrowserNodeInfo node) {
2243      return node != null && node.getNode() == node;
2244    }
2245  }
2246
2247
2248  /**
2249   * Returns whether we are in automatic expand mode.  This mode is used when
2250   * the user specifies a filter and all the nodes are automatically expanded.
2251   * @return <CODE>true</CODE> if we are in automatic expand mode and
2252   * <CODE>false</CODE> otherwise.
2253   */
2254  public boolean isAutomaticExpand()
2255  {
2256    return automaticExpand;
2257  }
2258
2259
2260  /**
2261   * Sets the automatic expand mode.
2262   * @param automaticExpand whether to expand automatically the nodes or not.
2263   */
2264  public void setAutomaticExpand(boolean automaticExpand)
2265  {
2266    this.automaticExpand = automaticExpand;
2267  }
2268}