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