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}