001/* 002 * Copyright 2014-2017 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2014-2017 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.ldap.sdk; 022 023 024 025import java.net.InetAddress; 026import java.net.UnknownHostException; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collections; 030import java.util.Hashtable; 031import java.util.List; 032import java.util.Map; 033import java.util.Properties; 034import java.util.StringTokenizer; 035import java.util.concurrent.atomic.AtomicLong; 036import java.util.concurrent.atomic.AtomicReference; 037import javax.naming.Context; 038import javax.naming.NamingEnumeration; 039import javax.naming.directory.Attribute; 040import javax.naming.directory.Attributes; 041import javax.naming.directory.InitialDirContext; 042import javax.net.SocketFactory; 043 044import com.unboundid.util.Debug; 045import com.unboundid.util.NotMutable; 046import com.unboundid.util.ObjectPair; 047import com.unboundid.util.ThreadLocalRandom; 048import com.unboundid.util.ThreadSafety; 049import com.unboundid.util.ThreadSafetyLevel; 050import com.unboundid.util.Validator; 051 052import static com.unboundid.ldap.sdk.LDAPMessages.*; 053 054 055 056/** 057 * This class provides a server set implementation that handles the case in 058 * which a given host name may resolve to multiple IP addresses. Note that 059 * while a setup like this is typically referred to as "round-robin DNS", this 060 * server set implementation does not strictly require DNS (as names may be 061 * resolved through alternate mechanisms like a hosts file or an alternate name 062 * service), and it does not strictly require round-robin use of those addresses 063 * (as alternate ordering mechanisms, like randomized or failover, may be used). 064 * <BR><BR> 065 * <H2>Example</H2> 066 * The following example demonstrates the process for creating a round-robin DNS 067 * server set for the case in which the hostname "directory.example.com" may be 068 * associated with multiple IP addresses, and the LDAP SDK should attempt to use 069 * them in a round robin manner. 070 * <PRE> 071 * // Define a number of variables that will be used by the server set. 072 * String hostname = "directory.example.com"; 073 * int port = 389; 074 * AddressSelectionMode selectionMode = 075 * AddressSelectionMode.ROUND_ROBIN; 076 * long cacheTimeoutMillis = 3600000L; // 1 hour 077 * String providerURL = "dns:"; // Default DNS config. 078 * SocketFactory socketFactory = null; // Default socket factory. 079 * LDAPConnectionOptions connectionOptions = null; // Default options. 080 * 081 * // Create the server set using the settings defined above. 082 * RoundRobinDNSServerSet serverSet = new RoundRobinDNSServerSet(hostname, 083 * port, selectionMode, cacheTimeoutMillis, providerURL, socketFactory, 084 * connectionOptions); 085 * 086 * // Verify that we can establish a single connection using the server set. 087 * LDAPConnection connection = serverSet.getConnection(); 088 * RootDSE rootDSEFromConnection = connection.getRootDSE(); 089 * connection.close(); 090 * 091 * // Verify that we can establish a connection pool using the server set. 092 * SimpleBindRequest bindRequest = 093 * new SimpleBindRequest("uid=pool.user,dc=example,dc=com", "password"); 094 * LDAPConnectionPool pool = 095 * new LDAPConnectionPool(serverSet, bindRequest, 10); 096 * RootDSE rootDSEFromPool = pool.getRootDSE(); 097 * pool.close(); 098 * </PRE> 099 */ 100@NotMutable() 101@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 102public final class RoundRobinDNSServerSet 103 extends ServerSet 104{ 105 /** 106 * The name of a system property that can be used to specify a comma-delimited 107 * list of IP addresses to use if resolution fails. This is intended 108 * primarily for testing purposes. 109 */ 110 static final String PROPERTY_DEFAULT_ADDRESSES = 111 RoundRobinDNSServerSet.class.getName() + ".defaultAddresses"; 112 113 114 115 /** 116 * An enum that defines the modes that may be used to select the order in 117 * which addresses should be used in attempts to establish connections. 118 */ 119 public enum AddressSelectionMode 120 { 121 /** 122 * The address selection mode that will cause addresses to be consistently 123 * attempted in the order they are retrieved from the name service. 124 */ 125 FAILOVER, 126 127 128 129 /** 130 * The address selection mode that will cause the order of addresses to be 131 * randomized for each attempt. 132 */ 133 RANDOM, 134 135 136 137 /** 138 * The address selection mode that will cause connection attempts to be made 139 * in a round-robin order. 140 */ 141 ROUND_ROBIN, 142 } 143 144 145 146 // The address selection mode that should be used if the provided hostname 147 // resolves to multiple addresses. 148 private final AddressSelectionMode selectionMode; 149 150 // A counter that will be used to handle round-robin ordering. 151 private final AtomicLong roundRobinCounter; 152 153 // A reference to an object that combines the resolved addresses with a 154 // timestamp indicating when the value should no longer be trusted. 155 private final AtomicReference<ObjectPair<InetAddress[],Long>> 156 resolvedAddressesWithTimeout; 157 158 // The properties that will be used to initialize the JNDI context, if any. 159 private final Hashtable<String,String> jndiProperties; 160 161 // The port number for the target server. 162 private final int port; 163 164 // The set of connection options to use for new connections. 165 private final LDAPConnectionOptions connectionOptions; 166 167 // The maximum length of time, in milliseconds, to cache resolved addresses. 168 private final long cacheTimeoutMillis; 169 170 // The socket factory to use to establish connections. 171 private final SocketFactory socketFactory; 172 173 // The hostname to be resolved. 174 private final String hostname; 175 176 // The provider URL to use to resolve names, if any. 177 private final String providerURL; 178 179 // The DNS record types that will be used to obtain the IP addresses for the 180 // specified hostname. 181 private final String[] dnsRecordTypes; 182 183 184 185 /** 186 * Creates a new round-robin DNS server set with the provided information. 187 * 188 * @param hostname The hostname to be resolved to one or more 189 * addresses. It must not be {@code null}. 190 * @param port The port to use to connect to the server. Note 191 * that even if the provided hostname resolves to 192 * multiple addresses, the same port must be used 193 * for all addresses. 194 * @param selectionMode The selection mode that should be used if the 195 * hostname resolves to multiple addresses. It 196 * must not be {@code null}. 197 * @param cacheTimeoutMillis The maximum length of time in milliseconds to 198 * cache addresses resolved from the provided 199 * hostname. Caching resolved addresses can 200 * result in better performance and can reduce the 201 * number of requests to the name service. A 202 * that is less than or equal to zero indicates 203 * that no caching should be used. 204 * @param providerURL The JNDI provider URL that should be used when 205 * communicating with the DNS server. If this is 206 * {@code null}, then the underlying system's 207 * name service mechanism will be used (which may 208 * make use of other services instead of or in 209 * addition to DNS). If this is non-{@code null}, 210 * then only DNS will be used to perform the name 211 * resolution. A value of "dns:" indicates that 212 * the underlying system's DNS configuration 213 * should be used. 214 * @param socketFactory The socket factory to use to establish the 215 * connections. It may be {@code null} if the 216 * JVM-default socket factory should be used. 217 * @param connectionOptions The set of connection options that should be 218 * used for the connections. It may be 219 * {@code null} if a default set of connection 220 * options should be used. 221 */ 222 public RoundRobinDNSServerSet(final String hostname, final int port, 223 final AddressSelectionMode selectionMode, 224 final long cacheTimeoutMillis, 225 final String providerURL, 226 final SocketFactory socketFactory, 227 final LDAPConnectionOptions connectionOptions) 228 { 229 this(hostname, port, selectionMode, cacheTimeoutMillis, providerURL, 230 null, null, socketFactory, connectionOptions); 231 } 232 233 234 235 /** 236 * Creates a new round-robin DNS server set with the provided information. 237 * 238 * @param hostname The hostname to be resolved to one or more 239 * addresses. It must not be {@code null}. 240 * @param port The port to use to connect to the server. Note 241 * that even if the provided hostname resolves to 242 * multiple addresses, the same port must be used 243 * for all addresses. 244 * @param selectionMode The selection mode that should be used if the 245 * hostname resolves to multiple addresses. It 246 * must not be {@code null}. 247 * @param cacheTimeoutMillis The maximum length of time in milliseconds to 248 * cache addresses resolved from the provided 249 * hostname. Caching resolved addresses can 250 * result in better performance and can reduce the 251 * number of requests to the name service. A 252 * that is less than or equal to zero indicates 253 * that no caching should be used. 254 * @param providerURL The JNDI provider URL that should be used when 255 * communicating with the DNS server.If both 256 * {@code providerURL} and {@code jndiProperties} 257 * are {@code null}, then then JNDI will not be 258 * used to interact with DNS and the hostname 259 * resolution will be performed via the underlying 260 * system's name service mechanism (which may make 261 * use of other services instead of or in addition 262 * to DNS).. If this is non-{@code null}, then 263 * only DNS will be used to perform the name 264 * resolution. A value of "dns:" indicates that 265 * the underlying system's DNS configuration 266 * should be used. 267 * @param jndiProperties A set of JNDI-related properties that should be 268 * be used when initializing the context for 269 * interacting with the DNS server via JNDI. If 270 * both {@code providerURL} and 271 * {@code jndiProperties} are {@code null}, then 272 * then JNDI will not be used to interact with 273 * DNS and the hostname resolution will be 274 * performed via the underlying system's name 275 * service mechanism (which may make use of other 276 * services instead of or in addition to DNS). If 277 * {@code providerURL} is {@code null} and 278 * {@code jndiProperties} is non-{@code null}, 279 * then the provided properties must specify the 280 * URL. 281 * @param dnsRecordTypes Specifies the types of DNS records that will be 282 * used to obtain the addresses for the specified 283 * hostname. This will only be used if at least 284 * one of {@code providerURL} and 285 * {@code jndiProperties} is non-{@code null}. If 286 * this is {@code null} or empty, then a default 287 * record type of "A" (indicating IPv4 addresses) 288 * will be used. 289 * @param socketFactory The socket factory to use to establish the 290 * connections. It may be {@code null} if the 291 * JVM-default socket factory should be used. 292 * @param connectionOptions The set of connection options that should be 293 * used for the connections. It may be 294 * {@code null} if a default set of connection 295 * options should be used. 296 */ 297 public RoundRobinDNSServerSet(final String hostname, final int port, 298 final AddressSelectionMode selectionMode, 299 final long cacheTimeoutMillis, 300 final String providerURL, 301 final Properties jndiProperties, 302 final String[] dnsRecordTypes, 303 final SocketFactory socketFactory, 304 final LDAPConnectionOptions connectionOptions) 305 { 306 Validator.ensureNotNull(hostname); 307 Validator.ensureTrue((port >= 1) && (port <= 65535)); 308 Validator.ensureNotNull(selectionMode); 309 310 this.hostname = hostname; 311 this.port = port; 312 this.selectionMode = selectionMode; 313 this.providerURL = providerURL; 314 315 if (jndiProperties == null) 316 { 317 if (providerURL == null) 318 { 319 this.jndiProperties = null; 320 } 321 else 322 { 323 this.jndiProperties = new Hashtable<String,String>(2); 324 this.jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY, 325 "com.sun.jndi.dns.DnsContextFactory"); 326 this.jndiProperties.put(Context.PROVIDER_URL, providerURL); 327 } 328 } 329 else 330 { 331 this.jndiProperties = 332 new Hashtable<String,String>(jndiProperties.size()+2); 333 for (final Map.Entry<Object,Object> e : jndiProperties.entrySet()) 334 { 335 this.jndiProperties.put(String.valueOf(e.getKey()), 336 String.valueOf(e.getValue())); 337 } 338 339 if (! this.jndiProperties.containsKey(Context.INITIAL_CONTEXT_FACTORY)) 340 { 341 this.jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY, 342 "com.sun.jndi.dns.DnsContextFactory"); 343 } 344 345 if ((! this.jndiProperties.containsKey(Context.PROVIDER_URL)) && 346 (providerURL != null)) 347 { 348 this.jndiProperties.put(Context.PROVIDER_URL, providerURL); 349 } 350 } 351 352 if (dnsRecordTypes == null) 353 { 354 this.dnsRecordTypes = new String[] { "A" }; 355 } 356 else 357 { 358 this.dnsRecordTypes = dnsRecordTypes; 359 } 360 361 if (cacheTimeoutMillis > 0L) 362 { 363 this.cacheTimeoutMillis = cacheTimeoutMillis; 364 } 365 else 366 { 367 this.cacheTimeoutMillis = 0L; 368 } 369 370 if (socketFactory == null) 371 { 372 this.socketFactory = SocketFactory.getDefault(); 373 } 374 else 375 { 376 this.socketFactory = socketFactory; 377 } 378 379 if (connectionOptions == null) 380 { 381 this.connectionOptions = new LDAPConnectionOptions(); 382 } 383 else 384 { 385 this.connectionOptions = connectionOptions; 386 } 387 388 roundRobinCounter = new AtomicLong(0L); 389 resolvedAddressesWithTimeout = 390 new AtomicReference<ObjectPair<InetAddress[],Long>>(); 391 } 392 393 394 395 /** 396 * Retrieves the hostname to be resolved. 397 * 398 * @return The hostname to be resolved. 399 */ 400 public String getHostname() 401 { 402 return hostname; 403 } 404 405 406 407 /** 408 * Retrieves the port to use to connect to the server. 409 * 410 * @return The port to use to connect to the server. 411 */ 412 public int getPort() 413 { 414 return port; 415 } 416 417 418 419 /** 420 * Retrieves the address selection mode that should be used if the provided 421 * hostname resolves to multiple addresses. 422 * 423 * @return The address selection 424 */ 425 public AddressSelectionMode getAddressSelectionMode() 426 { 427 return selectionMode; 428 } 429 430 431 432 /** 433 * Retrieves the length of time in milliseconds that resolved addresses may be 434 * cached. 435 * 436 * @return The length of time in milliseconds that resolved addresses may be 437 * cached, or zero if no caching should be performed. 438 */ 439 public long getCacheTimeoutMillis() 440 { 441 return cacheTimeoutMillis; 442 } 443 444 445 446 /** 447 * Retrieves the provider URL that should be used when interacting with DNS to 448 * resolve the hostname to its corresponding addresses. 449 * 450 * @return The provider URL that should be used when interacting with DNS to 451 * resolve the hostname to its corresponding addresses, or 452 * {@code null} if the system's configured naming service should be 453 * used. 454 */ 455 public String getProviderURL() 456 { 457 return providerURL; 458 } 459 460 461 462 /** 463 * Retrieves an unmodifiable map of properties that will be used to initialize 464 * the JNDI context used to interact with DNS. Note that the map returned 465 * will reflect the actual properties that will be used, and may not exactly 466 * match the properties provided when creating this server set. 467 * 468 * @return An unmodifiable map of properties that will be used to initialize 469 * the JNDI context used to interact with DNS, or {@code null} if 470 * JNDI will nto be used to interact with DNS. 471 */ 472 public Map<String,String> getJNDIProperties() 473 { 474 if (jndiProperties == null) 475 { 476 return null; 477 } 478 else 479 { 480 return Collections.unmodifiableMap(jndiProperties); 481 } 482 } 483 484 485 486 /** 487 * Retrieves an array of record types that will be requested if JNDI will be 488 * used to interact with DNS. 489 * 490 * @return An array of record types that will be requested if JNDI will be 491 * used to interact with DNS. 492 */ 493 public String[] getDNSRecordTypes() 494 { 495 return dnsRecordTypes; 496 } 497 498 499 500 /** 501 * Retrieves the socket factory that will be used to establish connections. 502 * This will not be {@code null}, even if no socket factory was provided when 503 * the server set was created. 504 * 505 * @return The socket factory that will be used to establish connections. 506 */ 507 public SocketFactory getSocketFactory() 508 { 509 return socketFactory; 510 } 511 512 513 514 /** 515 * Retrieves the set of connection options that will be used for underlying 516 * connections. This will not be {@code null}, even if no connection options 517 * object was provided when the server set was created. 518 * 519 * @return The set of connection options that will be used for underlying 520 * connections. 521 */ 522 public LDAPConnectionOptions getConnectionOptions() 523 { 524 return connectionOptions; 525 } 526 527 528 529 /** 530 * {@inheritDoc} 531 */ 532 @Override() 533 public LDAPConnection getConnection() 534 throws LDAPException 535 { 536 return getConnection(null); 537 } 538 539 540 541 /** 542 * {@inheritDoc} 543 */ 544 @Override() 545 public synchronized LDAPConnection getConnection( 546 final LDAPConnectionPoolHealthCheck healthCheck) 547 throws LDAPException 548 { 549 LDAPException firstException = null; 550 551 final LDAPConnection conn = 552 new LDAPConnection(socketFactory, connectionOptions); 553 for (final InetAddress a : orderAddresses(resolveHostname())) 554 { 555 boolean close = true; 556 try 557 { 558 conn.connect(hostname, a, port, 559 connectionOptions.getConnectTimeoutMillis()); 560 if (healthCheck != null) 561 { 562 healthCheck.ensureNewConnectionValid(conn); 563 } 564 close = false; 565 return conn; 566 } 567 catch (final LDAPException le) 568 { 569 Debug.debugException(le); 570 if (firstException == null) 571 { 572 firstException = le; 573 } 574 } 575 finally 576 { 577 if (close) 578 { 579 conn.close(); 580 } 581 } 582 } 583 584 throw firstException; 585 } 586 587 588 589 /** 590 * Resolve the hostname to its corresponding addresses. 591 * 592 * @return The addresses resolved from the hostname. 593 * 594 * @throws LDAPException If 595 */ 596 InetAddress[] resolveHostname() 597 throws LDAPException 598 { 599 // First, see if we can use the cached addresses. 600 final ObjectPair<InetAddress[],Long> pair = 601 resolvedAddressesWithTimeout.get(); 602 if (pair != null) 603 { 604 if (pair.getSecond() <= System.currentTimeMillis()) 605 { 606 return pair.getFirst(); 607 } 608 } 609 610 611 // Try to resolve the address. 612 InetAddress[] addresses = null; 613 try 614 { 615 if (jndiProperties == null) 616 { 617 addresses = InetAddress.getAllByName(hostname); 618 } 619 else 620 { 621 Attributes attributes = null; 622 final InitialDirContext context = new InitialDirContext(jndiProperties); 623 try 624 { 625 attributes = context.getAttributes(hostname, dnsRecordTypes); 626 } 627 finally 628 { 629 context.close(); 630 } 631 632 if (attributes != null) 633 { 634 final ArrayList<InetAddress> addressList = 635 new ArrayList<InetAddress>(10); 636 for (final String recordType : dnsRecordTypes) 637 { 638 final Attribute a = attributes.get(recordType); 639 if (a != null) 640 { 641 final NamingEnumeration<?> values = a.getAll(); 642 while (values.hasMore()) 643 { 644 final Object value = values.next(); 645 addressList.add(getInetAddressForIP(String.valueOf(value))); 646 } 647 } 648 } 649 650 if (! addressList.isEmpty()) 651 { 652 addresses = new InetAddress[addressList.size()]; 653 addressList.toArray(addresses); 654 } 655 } 656 } 657 } 658 catch (final Exception e) 659 { 660 Debug.debugException(e); 661 addresses = getDefaultAddresses(); 662 } 663 664 665 // If we were able to resolve the hostname, then cache and return the 666 // resolved addresses. 667 if ((addresses != null) && (addresses.length > 0)) 668 { 669 final long timeoutTime; 670 if (cacheTimeoutMillis > 0L) 671 { 672 timeoutTime = System.currentTimeMillis() + cacheTimeoutMillis; 673 } 674 else 675 { 676 timeoutTime = System.currentTimeMillis() - 1L; 677 } 678 679 resolvedAddressesWithTimeout.set(new ObjectPair<InetAddress[],Long>( 680 addresses, timeoutTime)); 681 return addresses; 682 } 683 684 685 // If we've gotten here, then we couldn't resolve the hostname. If we have 686 // cached addresses, then use them even though the timeout has expired 687 // because that's better than nothing. 688 if (pair != null) 689 { 690 return pair.getFirst(); 691 } 692 693 throw new LDAPException(ResultCode.CONNECT_ERROR, 694 ERR_ROUND_ROBIN_DNS_SERVER_SET_CANNOT_RESOLVE.get(hostname)); 695 } 696 697 698 699 /** 700 * Orders the provided array of InetAddress objects to reflect the order in 701 * which the addresses should be used to try to create a new connection. 702 * 703 * @param addresses The array of addresses to be ordered. 704 * 705 * @return A list containing the ordered addresses. 706 */ 707 List<InetAddress> orderAddresses(final InetAddress[] addresses) 708 { 709 final ArrayList<InetAddress> l = 710 new ArrayList<InetAddress>(addresses.length); 711 712 switch (selectionMode) 713 { 714 case RANDOM: 715 l.addAll(Arrays.asList(addresses)); 716 Collections.shuffle(l, ThreadLocalRandom.get()); 717 break; 718 719 case ROUND_ROBIN: 720 final int index = 721 (int) (roundRobinCounter.getAndIncrement() % addresses.length); 722 for (int i=index; i < addresses.length; i++) 723 { 724 l.add(addresses[i]); 725 } 726 for (int i=0; i < index; i++) 727 { 728 l.add(addresses[i]); 729 } 730 break; 731 732 case FAILOVER: 733 default: 734 // We'll use the addresses in the same order we originally got them. 735 l.addAll(Arrays.asList(addresses)); 736 break; 737 } 738 739 return l; 740 } 741 742 743 744 /** 745 * Retrieves a default set of addresses that may be used for testing. 746 * 747 * @return A default set of addresses that may be used for testing. 748 */ 749 InetAddress[] getDefaultAddresses() 750 { 751 final String defaultAddrsStr = 752 System.getProperty(PROPERTY_DEFAULT_ADDRESSES); 753 if (defaultAddrsStr == null) 754 { 755 return null; 756 } 757 758 final StringTokenizer tokenizer = 759 new StringTokenizer(defaultAddrsStr, " ,"); 760 final InetAddress[] addresses = new InetAddress[tokenizer.countTokens()]; 761 for (int i=0; i < addresses.length; i++) 762 { 763 try 764 { 765 addresses[i] = getInetAddressForIP(tokenizer.nextToken()); 766 } 767 catch (final Exception e) 768 { 769 Debug.debugException(e); 770 return null; 771 } 772 } 773 774 return addresses; 775 } 776 777 778 779 /** 780 * Retrieves an InetAddress object with the configured hostname and the 781 * provided IP address. 782 * 783 * @param ipAddress The string representation of the IP address to use in 784 * the returned InetAddress. 785 * 786 * @return The created InetAddress. 787 * 788 * @throws UnknownHostException If the provided string does not represent a 789 * valid IPv4 or IPv6 address. 790 */ 791 private InetAddress getInetAddressForIP(final String ipAddress) 792 throws UnknownHostException 793 { 794 // We want to create an InetAddress that has the provided hostname and the 795 // specified IP address. To do that, we need to use 796 // InetAddress.getByAddress. But that requires the IP address to be 797 // specified as a byte array, and the easiest way to convert an IP address 798 // string to a byte array is to use InetAddress.getByName. 799 final InetAddress byName = InetAddress.getByName(String.valueOf(ipAddress)); 800 return InetAddress.getByAddress(hostname, byName.getAddress()); 801 } 802 803 804 805 /** 806 * {@inheritDoc} 807 */ 808 @Override() 809 public void toString(final StringBuilder buffer) 810 { 811 buffer.append("RoundRobinDNSServerSet(hostname='"); 812 buffer.append(hostname); 813 buffer.append("', port="); 814 buffer.append(port); 815 buffer.append(", addressSelectionMode="); 816 buffer.append(selectionMode.name()); 817 buffer.append(", cacheTimeoutMillis="); 818 buffer.append(cacheTimeoutMillis); 819 820 if (providerURL != null) 821 { 822 buffer.append(", providerURL='"); 823 buffer.append(providerURL); 824 buffer.append('\''); 825 } 826 827 buffer.append(')'); 828 } 829}