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