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