001 /*
002 * Copyright 2014-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2014-2016 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 */
021 package com.unboundid.ldap.sdk;
022
023
024
025 import java.net.InetAddress;
026 import java.net.UnknownHostException;
027 import java.util.ArrayList;
028 import java.util.Arrays;
029 import java.util.Collections;
030 import java.util.Hashtable;
031 import java.util.List;
032 import java.util.Map;
033 import java.util.Properties;
034 import java.util.StringTokenizer;
035 import java.util.concurrent.atomic.AtomicLong;
036 import java.util.concurrent.atomic.AtomicReference;
037 import javax.naming.Context;
038 import javax.naming.NamingEnumeration;
039 import javax.naming.directory.Attribute;
040 import javax.naming.directory.Attributes;
041 import javax.naming.directory.InitialDirContext;
042 import javax.net.SocketFactory;
043
044 import com.unboundid.util.Debug;
045 import com.unboundid.util.NotMutable;
046 import com.unboundid.util.ObjectPair;
047 import com.unboundid.util.ThreadLocalRandom;
048 import com.unboundid.util.ThreadSafety;
049 import com.unboundid.util.ThreadSafetyLevel;
050 import com.unboundid.util.Validator;
051
052 import 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)
102 public 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 }