001 /*
002 * Copyright 2007-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-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.io.Serializable;
026 import java.nio.ByteBuffer;
027 import java.util.ArrayList;
028
029
030 import static com.unboundid.ldap.sdk.LDAPMessages.*;
031 import static com.unboundid.util.Debug.*;
032 import static com.unboundid.util.StaticUtils.*;
033 import static com.unboundid.util.Validator.*;
034
035
036
037 /**
038 * This class provides a data structure for interacting with LDAP URLs. It may
039 * be used to encode and decode URLs, as well as access the various elements
040 * that they contain. Note that this implementation currently does not support
041 * the use of extensions in an LDAP URL.
042 * <BR><BR>
043 * The components that may be included in an LDAP URL include:
044 * <UL>
045 * <LI>Scheme -- This specifies the protocol to use when communicating with
046 * the server. The official LDAP URL specification only allows a scheme
047 * of "{@code ldap}", but this implementation also supports the use of the
048 * "{@code ldaps}" scheme to indicate that clients should attempt to
049 * perform SSL-based communication with the target server (LDAPS) rather
050 * than unencrypted LDAP. It will also accept "{@code ldapi}", which is
051 * LDAP over UNIX domain sockets, although the LDAP SDK does not directly
052 * support that mechanism of communication.</LI>
053 * <LI>Host -- This specifies the address of the directory server to which the
054 * URL refers. If no host is provided, then it is expected that the
055 * client has some prior knowledge of the host (it often implies the same
056 * server from which the URL was retrieved).</LI>
057 * <LI>Port -- This specifies the port of the directory server to which the
058 * URL refers. If no host or port is provided, then it is assumed that
059 * the client has some prior knowledge of the instance to use (it often
060 * implies the same instance from which the URL was retrieved). If a host
061 * is provided without a port, then it should be assumed that the standard
062 * LDAP port of 389 should be used (or the standard LDAPS port of 636 if
063 * the scheme is "{@code ldaps}", or a value of 0 if the scheme is
064 * "{@code ldapi}").</LI>
065 * <LI>Base DN -- This specifies the base DN for the URL. If no base DN is
066 * provided, then a default of the null DN should be assumed.</LI>
067 * <LI>Requested attributes -- This specifies the set of requested attributes
068 * for the URL. If no attributes are specified, then the behavior should
069 * be the same as if no attributes had been provided for a search request
070 * (i.e., all user attributes should be included).
071 * <BR><BR>
072 * In the string representation of an LDAP URL, the names of the requested
073 * attributes (if more than one is provided) should be separated by
074 * commas.</LI>
075 * <LI>Scope -- This specifies the scope for the URL. It should be one of the
076 * standard scope values as defined in the {@code SearchRequest}
077 * class. If no scope is provided, then it should be assumed that a
078 * scope of {@code SearchScope#BASE} should be used.
079 * <BR><BR>
080 * In the string representation, the names of the scope values that are
081 * allowed include:
082 * <UL>
083 * <LI>base -- Equivalent to {@code SearchScope#BASE}.</LI>
084 * <LI>one -- Equivalent to {@code SearchScope#ONE}.</LI>
085 * <LI>sub -- Equivalent to {@code SearchScope#SUB}.</LI>
086 * <LI>subordinates -- Equivalent to
087 * {@code SearchScope#SUBORDINATE_SUBTREE}.</LI>
088 * </UL></LI>
089 * <LI>Filter -- This specifies the filter for the URL. If no filter is
090 * provided, then a default of "{@code (objectClass=*)}" should be
091 * assumed.</LI>
092 * </UL>
093 * An LDAP URL encapsulates many of the properties of a search request, and in
094 * fact the {@code LDAPURL#toSearchRequest} method may be used to create a
095 * {@code SearchRequest} object from an LDAP URL.
096 * <BR><BR>
097 * See <A HREF="http://www.ietf.org/rfc/rfc4516.txt">RFC 4516</A> for a complete
098 * description of the LDAP URL syntax. Some examples of LDAP URLs include:
099 * <UL>
100 * <LI>{@code ldap://} -- This is the smallest possible LDAP URL that can be
101 * represented. The default values will be used for all components other
102 * than the scheme.</LI>
103 * <LI>{@code
104 * ldap://server.example.com:1234/dc=example,dc=com?cn,sn?sub?(uid=john)}
105 * -- This is an example of a URL containing all of the elements. The
106 * scheme is "{@code ldap}", the host is "{@code server.example.com}",
107 * the port is "{@code 1234}", the base DN is "{@code dc=example,dc=com}",
108 * the requested attributes are "{@code cn}" and "{@code sn}", the scope
109 * is "{@code sub}" (which indicates a subtree scope equivalent to
110 * {@code SearchScope#SUB}), and a filter of
111 * "{@code (uid=john)}".</LI>
112 * </UL>
113 */
114 public final class LDAPURL
115 implements Serializable
116 {
117 /**
118 * The default filter that will be used if none is provided.
119 */
120 private static final Filter DEFAULT_FILTER =
121 Filter.createPresenceFilter("objectClass");
122
123
124
125 /**
126 * The default port number that will be used for LDAP URLs if none is
127 * provided.
128 */
129 public static final int DEFAULT_LDAP_PORT = 389;
130
131
132
133 /**
134 * The default port number that will be used for LDAPS URLs if none is
135 * provided.
136 */
137 public static final int DEFAULT_LDAPS_PORT = 636;
138
139
140
141 /**
142 * The default port number that will be used for LDAPI URLs if none is
143 * provided.
144 */
145 public static final int DEFAULT_LDAPI_PORT = 0;
146
147
148
149 /**
150 * The default scope that will be used if none is provided.
151 */
152 private static final SearchScope DEFAULT_SCOPE = SearchScope.BASE;
153
154
155
156 /**
157 * The default base DN that will be used if none is provided.
158 */
159 private static final DN DEFAULT_BASE_DN = DN.NULL_DN;
160
161
162
163 /**
164 * The default set of attributes that will be used if none is provided.
165 */
166 private static final String[] DEFAULT_ATTRIBUTES = NO_STRINGS;
167
168
169
170 /**
171 * The serial version UID for this serializable class.
172 */
173 private static final long serialVersionUID = 3420786933570240493L;
174
175
176
177 // Indicates whether the attribute list was provided in the URL.
178 private final boolean attributesProvided;
179
180 // Indicates whether the base DN was provided in the URL.
181 private final boolean baseDNProvided;
182
183 // Indicates whether the filter was provided in the URL.
184 private final boolean filterProvided;
185
186 // Indicates whether the port was provided in the URL.
187 private final boolean portProvided;
188
189 // Indicates whether the scope was provided in the URL.
190 private final boolean scopeProvided;
191
192 // The base DN used by this URL.
193 private final DN baseDN;
194
195 // The filter used by this URL.
196 private final Filter filter;
197
198 // The port used by this URL.
199 private final int port;
200
201 // The search scope used by this URL.
202 private final SearchScope scope;
203
204 // The host used by this URL.
205 private final String host;
206
207 // The normalized representation of this LDAP URL.
208 private volatile String normalizedURLString;
209
210 // The scheme used by this LDAP URL. The standard only accepts "ldap", but
211 // we will also accept "ldaps" and "ldapi".
212 private final String scheme;
213
214 // The string representation of this LDAP URL.
215 private final String urlString;
216
217 // The set of attributes included in this URL.
218 private final String[] attributes;
219
220
221
222 /**
223 * Creates a new LDAP URL from the provided string representation.
224 *
225 * @param urlString The string representation for this LDAP URL. It must
226 * not be {@code null}.
227 *
228 * @throws LDAPException If the provided URL string cannot be parsed as an
229 * LDAP URL.
230 */
231 public LDAPURL(final String urlString)
232 throws LDAPException
233 {
234 ensureNotNull(urlString);
235
236 this.urlString = urlString;
237
238
239 // Find the location of the first colon. It should mark the end of the
240 // scheme.
241 final int colonPos = urlString.indexOf("://");
242 if (colonPos < 0)
243 {
244 throw new LDAPException(ResultCode.DECODING_ERROR,
245 ERR_LDAPURL_NO_COLON_SLASHES.get());
246 }
247
248 scheme = toLowerCase(urlString.substring(0, colonPos));
249 final int defaultPort;
250 if (scheme.equals("ldap"))
251 {
252 defaultPort = DEFAULT_LDAP_PORT;
253 }
254 else if (scheme.equals("ldaps"))
255 {
256 defaultPort = DEFAULT_LDAPS_PORT;
257 }
258 else if (scheme.equals("ldapi"))
259 {
260 defaultPort = DEFAULT_LDAPI_PORT;
261 }
262 else
263 {
264 throw new LDAPException(ResultCode.DECODING_ERROR,
265 ERR_LDAPURL_INVALID_SCHEME.get(scheme));
266 }
267
268
269 // Look for the first slash after the "://". It will designate the end of
270 // the hostport section.
271 final int slashPos = urlString.indexOf('/', colonPos+3);
272 if (slashPos < 0)
273 {
274 // This is fine. It just means that the URL won't have a base DN,
275 // attribute list, scope, or filter, and that the rest of the value is
276 // the hostport element.
277 baseDN = DEFAULT_BASE_DN;
278 baseDNProvided = false;
279 attributes = DEFAULT_ATTRIBUTES;
280 attributesProvided = false;
281 scope = DEFAULT_SCOPE;
282 scopeProvided = false;
283 filter = DEFAULT_FILTER;
284 filterProvided = false;
285
286 final String hostPort = urlString.substring(colonPos+3);
287 final StringBuilder hostBuffer = new StringBuilder(hostPort.length());
288 final int portValue = decodeHostPort(hostPort, hostBuffer);
289 if (portValue < 0)
290 {
291 port = defaultPort;
292 portProvided = false;
293 }
294 else
295 {
296 port = portValue;
297 portProvided = true;
298 }
299
300 if (hostBuffer.length() == 0)
301 {
302 host = null;
303 }
304 else
305 {
306 host = hostBuffer.toString();
307 }
308 return;
309 }
310
311 final String hostPort = urlString.substring(colonPos+3, slashPos);
312 final StringBuilder hostBuffer = new StringBuilder(hostPort.length());
313 final int portValue = decodeHostPort(hostPort, hostBuffer);
314 if (portValue < 0)
315 {
316 port = defaultPort;
317 portProvided = false;
318 }
319 else
320 {
321 port = portValue;
322 portProvided = true;
323 }
324
325 if (hostBuffer.length() == 0)
326 {
327 host = null;
328 }
329 else
330 {
331 host = hostBuffer.toString();
332 }
333
334
335 // Look for the first question mark after the slash. It will designate the
336 // end of the base DN.
337 final int questionMarkPos = urlString.indexOf('?', slashPos+1);
338 if (questionMarkPos < 0)
339 {
340 // This is fine. It just means that the URL won't have an attribute list,
341 // scope, or filter, and that the rest of the value is the base DN.
342 attributes = DEFAULT_ATTRIBUTES;
343 attributesProvided = false;
344 scope = DEFAULT_SCOPE;
345 scopeProvided = false;
346 filter = DEFAULT_FILTER;
347 filterProvided = false;
348
349 baseDN = new DN(percentDecode(urlString.substring(slashPos+1)));
350 baseDNProvided = (! baseDN.isNullDN());
351 return;
352 }
353
354 baseDN = new DN(percentDecode(urlString.substring(slashPos+1,
355 questionMarkPos)));
356 baseDNProvided = (! baseDN.isNullDN());
357
358
359 // Look for the next question mark. It will designate the end of the
360 // attribute list.
361 final int questionMark2Pos = urlString.indexOf('?', questionMarkPos+1);
362 if (questionMark2Pos < 0)
363 {
364 // This is fine. It just means that the URL won't have a scope or filter,
365 // and that the rest of the value is the attribute list.
366 scope = DEFAULT_SCOPE;
367 scopeProvided = false;
368 filter = DEFAULT_FILTER;
369 filterProvided = false;
370
371 attributes = decodeAttributes(urlString.substring(questionMarkPos+1));
372 attributesProvided = (attributes.length > 0);
373 return;
374 }
375
376 attributes = decodeAttributes(urlString.substring(questionMarkPos+1,
377 questionMark2Pos));
378 attributesProvided = (attributes.length > 0);
379
380
381 // Look for the next question mark. It will designate the end of the scope.
382 final int questionMark3Pos = urlString.indexOf('?', questionMark2Pos+1);
383 if (questionMark3Pos < 0)
384 {
385 // This is fine. It just means that the URL won't have a filter, and that
386 // the rest of the value is the scope.
387 filter = DEFAULT_FILTER;
388 filterProvided = false;
389
390 final String scopeStr =
391 toLowerCase(urlString.substring(questionMark2Pos+1));
392 if (scopeStr.length() == 0)
393 {
394 scope = SearchScope.BASE;
395 scopeProvided = false;
396 }
397 else if (scopeStr.equals("base"))
398 {
399 scope = SearchScope.BASE;
400 scopeProvided = true;
401 }
402 else if (scopeStr.equals("one"))
403 {
404 scope = SearchScope.ONE;
405 scopeProvided = true;
406 }
407 else if (scopeStr.equals("sub"))
408 {
409 scope = SearchScope.SUB;
410 scopeProvided = true;
411 }
412 else if (scopeStr.equals("subord") || scopeStr.equals("subordinates"))
413 {
414 scope = SearchScope.SUBORDINATE_SUBTREE;
415 scopeProvided = true;
416 }
417 else
418 {
419 throw new LDAPException(ResultCode.DECODING_ERROR,
420 ERR_LDAPURL_INVALID_SCOPE.get(scopeStr));
421 }
422 return;
423 }
424
425 final String scopeStr =
426 toLowerCase(urlString.substring(questionMark2Pos+1, questionMark3Pos));
427 if (scopeStr.length() == 0)
428 {
429 scope = SearchScope.BASE;
430 scopeProvided = false;
431 }
432 else if (scopeStr.equals("base"))
433 {
434 scope = SearchScope.BASE;
435 scopeProvided = true;
436 }
437 else if (scopeStr.equals("one"))
438 {
439 scope = SearchScope.ONE;
440 scopeProvided = true;
441 }
442 else if (scopeStr.equals("sub"))
443 {
444 scope = SearchScope.SUB;
445 scopeProvided = true;
446 }
447 else if (scopeStr.equals("subord") || scopeStr.equals("subordinates"))
448 {
449 scope = SearchScope.SUBORDINATE_SUBTREE;
450 scopeProvided = true;
451 }
452 else
453 {
454 throw new LDAPException(ResultCode.DECODING_ERROR,
455 ERR_LDAPURL_INVALID_SCOPE.get(scopeStr));
456 }
457
458
459 // The remainder of the value must be the filter.
460 final String filterStr =
461 percentDecode(urlString.substring(questionMark3Pos+1));
462 if (filterStr.length() == 0)
463 {
464 filter = DEFAULT_FILTER;
465 filterProvided = false;
466 }
467 else
468 {
469 filter = Filter.create(filterStr);
470 filterProvided = true;
471 }
472 }
473
474
475
476 /**
477 * Creates a new LDAP URL with the provided information.
478 *
479 * @param scheme The scheme for this LDAP URL. It must not be
480 * {@code null} and must be either "ldap", "ldaps", or
481 * "ldapi".
482 * @param host The host for this LDAP URL. It may be {@code null} if
483 * no host is to be included.
484 * @param port The port for this LDAP URL. It may be {@code null} if
485 * no port is to be included. If it is provided, it must
486 * be between 1 and 65535, inclusive.
487 * @param baseDN The base DN for this LDAP URL. It may be {@code null}
488 * if no base DN is to be included.
489 * @param attributes The set of requested attributes for this LDAP URL. It
490 * may be {@code null} or empty if no attribute list is to
491 * be included.
492 * @param scope The scope for this LDAP URL. It may be {@code null} if
493 * no scope is to be included. Otherwise, it must be a
494 * value between zero and three, inclusive.
495 * @param filter The filter for this LDAP URL. It may be {@code null}
496 * if no filter is to be included.
497 *
498 * @throws LDAPException If there is a problem with any of the provided
499 * arguments.
500 */
501 public LDAPURL(final String scheme, final String host, final Integer port,
502 final DN baseDN, final String[] attributes,
503 final SearchScope scope, final Filter filter)
504 throws LDAPException
505 {
506 ensureNotNull(scheme);
507
508 final StringBuilder buffer = new StringBuilder();
509
510 this.scheme = toLowerCase(scheme);
511 final int defaultPort;
512 if (scheme.equals("ldap"))
513 {
514 defaultPort = DEFAULT_LDAP_PORT;
515 }
516 else if (scheme.equals("ldaps"))
517 {
518 defaultPort = DEFAULT_LDAPS_PORT;
519 }
520 else if (scheme.equals("ldapi"))
521 {
522 defaultPort = DEFAULT_LDAPI_PORT;
523 }
524 else
525 {
526 throw new LDAPException(ResultCode.DECODING_ERROR,
527 ERR_LDAPURL_INVALID_SCHEME.get(scheme));
528 }
529
530 buffer.append(scheme);
531 buffer.append("://");
532
533 if ((host == null) || (host.length() == 0))
534 {
535 this.host = null;
536 }
537 else
538 {
539 this.host = host;
540 buffer.append(host);
541 }
542
543 if (port == null)
544 {
545 this.port = defaultPort;
546 portProvided = false;
547 }
548 else
549 {
550 this.port = port;
551 portProvided = true;
552 buffer.append(':');
553 buffer.append(port);
554
555 if ((port < 1) || (port > 65535))
556 {
557 throw new LDAPException(ResultCode.PARAM_ERROR,
558 ERR_LDAPURL_INVALID_PORT.get(port));
559 }
560 }
561
562 buffer.append('/');
563 if (baseDN == null)
564 {
565 this.baseDN = DEFAULT_BASE_DN;
566 baseDNProvided = false;
567 }
568 else
569 {
570 this.baseDN = baseDN;
571 baseDNProvided = true;
572 percentEncode(baseDN.toString(), buffer);
573 }
574
575 final boolean continueAppending;
576 if (((attributes == null) || (attributes.length == 0)) && (scope == null) &&
577 (filter == null))
578 {
579 continueAppending = false;
580 }
581 else
582 {
583 continueAppending = true;
584 }
585
586 if (continueAppending)
587 {
588 buffer.append('?');
589 }
590 if ((attributes == null) || (attributes.length == 0))
591 {
592 this.attributes = DEFAULT_ATTRIBUTES;
593 attributesProvided = false;
594 }
595 else
596 {
597 this.attributes = attributes;
598 attributesProvided = true;
599
600 for (int i=0; i < attributes.length; i++)
601 {
602 if (i > 0)
603 {
604 buffer.append(',');
605 }
606 buffer.append(attributes[i]);
607 }
608 }
609
610 if (continueAppending)
611 {
612 buffer.append('?');
613 }
614 if (scope == null)
615 {
616 this.scope = DEFAULT_SCOPE;
617 scopeProvided = false;
618 }
619 else
620 {
621 switch (scope.intValue())
622 {
623 case 0:
624 this.scope = scope;
625 scopeProvided = true;
626 buffer.append("base");
627 break;
628 case 1:
629 this.scope = scope;
630 scopeProvided = true;
631 buffer.append("one");
632 break;
633 case 2:
634 this.scope = scope;
635 scopeProvided = true;
636 buffer.append("sub");
637 break;
638 case 3:
639 this.scope = scope;
640 scopeProvided = true;
641 buffer.append("subordinates");
642 break;
643 default:
644 throw new LDAPException(ResultCode.PARAM_ERROR,
645 ERR_LDAPURL_INVALID_SCOPE_VALUE.get(scope));
646 }
647 }
648
649 if (continueAppending)
650 {
651 buffer.append('?');
652 }
653 if (filter == null)
654 {
655 this.filter = DEFAULT_FILTER;
656 filterProvided = false;
657 }
658 else
659 {
660 this.filter = filter;
661 filterProvided = true;
662 percentEncode(filter.toString(), buffer);
663 }
664
665 urlString = buffer.toString();
666 }
667
668
669
670 /**
671 * Decodes the provided string as a host and optional port number.
672 *
673 * @param hostPort The string to be decoded.
674 * @param hostBuffer The buffer to which the decoded host address will be
675 * appended.
676 *
677 * @return The port number decoded from the provided string, or -1 if there
678 * was no port number.
679 *
680 * @throws LDAPException If the provided string cannot be decoded as a
681 * hostport element.
682 */
683 private static int decodeHostPort(final String hostPort,
684 final StringBuilder hostBuffer)
685 throws LDAPException
686 {
687 final int length = hostPort.length();
688 if (length == 0)
689 {
690 // It's an empty string, so we'll just use the defaults.
691 return -1;
692 }
693
694 if (hostPort.charAt(0) == '[')
695 {
696 // It starts with a square bracket, which means that the address is an
697 // IPv6 literal address. Find the closing bracket, and the address
698 // will be inside them.
699 final int closingBracketPos = hostPort.indexOf(']');
700 if (closingBracketPos < 0)
701 {
702 throw new LDAPException(ResultCode.DECODING_ERROR,
703 ERR_LDAPURL_IPV6_HOST_MISSING_BRACKET.get());
704 }
705
706 hostBuffer.append(hostPort.substring(1, closingBracketPos).trim());
707 if (hostBuffer.length() == 0)
708 {
709 throw new LDAPException(ResultCode.DECODING_ERROR,
710 ERR_LDAPURL_IPV6_HOST_EMPTY.get());
711 }
712
713 // The closing bracket must either be the end of the hostport element
714 // (in which case we'll use the default port), or it must be followed by
715 // a colon and an integer (which will be the port).
716 if (closingBracketPos == (length - 1))
717 {
718 return -1;
719 }
720 else
721 {
722 if (hostPort.charAt(closingBracketPos+1) != ':')
723 {
724 throw new LDAPException(ResultCode.DECODING_ERROR,
725 ERR_LDAPURL_IPV6_HOST_UNEXPECTED_CHAR.get(
726 hostPort.charAt(closingBracketPos+1)));
727 }
728 else
729 {
730 try
731 {
732 final int decodedPort =
733 Integer.parseInt(hostPort.substring(closingBracketPos+2));
734 if ((decodedPort >= 1) && (decodedPort <= 65535))
735 {
736 return decodedPort;
737 }
738 else
739 {
740 throw new LDAPException(ResultCode.DECODING_ERROR,
741 ERR_LDAPURL_INVALID_PORT.get(
742 decodedPort));
743 }
744 }
745 catch (NumberFormatException nfe)
746 {
747 debugException(nfe);
748 throw new LDAPException(ResultCode.DECODING_ERROR,
749 ERR_LDAPURL_PORT_NOT_INT.get(hostPort),
750 nfe);
751 }
752 }
753 }
754 }
755
756
757 // If we've gotten here, then the address is either a resolvable name or an
758 // IPv4 address. If there is a colon in the string, then it will separate
759 // the address from the port. Otherwise, the remaining value will be the
760 // address and we'll use the default port.
761 final int colonPos = hostPort.indexOf(':');
762 if (colonPos < 0)
763 {
764 hostBuffer.append(hostPort);
765 return -1;
766 }
767 else
768 {
769 try
770 {
771 final int decodedPort =
772 Integer.parseInt(hostPort.substring(colonPos+1));
773 if ((decodedPort >= 1) && (decodedPort <= 65535))
774 {
775 hostBuffer.append(hostPort.substring(0, colonPos));
776 return decodedPort;
777 }
778 else
779 {
780 throw new LDAPException(ResultCode.DECODING_ERROR,
781 ERR_LDAPURL_INVALID_PORT.get(decodedPort));
782 }
783 }
784 catch (NumberFormatException nfe)
785 {
786 debugException(nfe);
787 throw new LDAPException(ResultCode.DECODING_ERROR,
788 ERR_LDAPURL_PORT_NOT_INT.get(hostPort), nfe);
789 }
790 }
791 }
792
793
794
795 /**
796 * Decodes the contents of the provided string as an attribute list.
797 *
798 * @param s The string to decode as an attribute list.
799 *
800 * @return The array of decoded attribute names.
801 *
802 * @throws LDAPException If an error occurred while attempting to decode the
803 * attribute list.
804 */
805 private static String[] decodeAttributes(final String s)
806 throws LDAPException
807 {
808 final int length = s.length();
809 if (length == 0)
810 {
811 return DEFAULT_ATTRIBUTES;
812 }
813
814 final ArrayList<String> attrList = new ArrayList<String>();
815 int startPos = 0;
816 while (startPos < length)
817 {
818 final int commaPos = s.indexOf(',', startPos);
819 if (commaPos < 0)
820 {
821 // There are no more commas, so there can only be one attribute left.
822 final String attrName = s.substring(startPos).trim();
823 if (attrName.length() == 0)
824 {
825 // This is only acceptable if the attribute list is empty (there was
826 // probably a space in the attribute list string, which is technically
827 // not allowed, but we'll accept it). If the attribute list is not
828 // empty, then there were two consecutive commas, which is not
829 // allowed.
830 if (attrList.isEmpty())
831 {
832 return DEFAULT_ATTRIBUTES;
833 }
834 else
835 {
836 throw new LDAPException(ResultCode.DECODING_ERROR,
837 ERR_LDAPURL_ATTRLIST_ENDS_WITH_COMMA.get());
838 }
839 }
840 else
841 {
842 attrList.add(attrName);
843 break;
844 }
845 }
846 else
847 {
848 final String attrName = s.substring(startPos, commaPos).trim();
849 if (attrName.length() == 0)
850 {
851 throw new LDAPException(ResultCode.DECODING_ERROR,
852 ERR_LDAPURL_ATTRLIST_EMPTY_ATTRIBUTE.get());
853 }
854 else
855 {
856 attrList.add(attrName);
857 startPos = commaPos+1;
858 if (startPos >= length)
859 {
860 throw new LDAPException(ResultCode.DECODING_ERROR,
861 ERR_LDAPURL_ATTRLIST_ENDS_WITH_COMMA.get());
862 }
863 }
864 }
865 }
866
867 final String[] attributes = new String[attrList.size()];
868 attrList.toArray(attributes);
869 return attributes;
870 }
871
872
873
874 /**
875 * Decodes any percent-encoded values that may be contained in the provided
876 * string.
877 *
878 * @param s The string to be decoded.
879 *
880 * @return The percent-decoded form of the provided string.
881 *
882 * @throws LDAPException If a problem occurs while attempting to decode the
883 * provided string.
884 */
885 public static String percentDecode(final String s)
886 throws LDAPException
887 {
888 // First, see if there are any percent characters at all in the provided
889 // string. If not, then just return the string as-is.
890 int firstPercentPos = -1;
891 final int length = s.length();
892 for (int i=0; i < length; i++)
893 {
894 if (s.charAt(i) == '%')
895 {
896 firstPercentPos = i;
897 break;
898 }
899 }
900
901 if (firstPercentPos < 0)
902 {
903 return s;
904 }
905
906 int pos = firstPercentPos;
907 final StringBuilder buffer = new StringBuilder(2 * length);
908 buffer.append(s.substring(0, firstPercentPos));
909
910 while (pos < length)
911 {
912 final char c = s.charAt(pos++);
913 if (c == '%')
914 {
915 if (pos >= length)
916 {
917 throw new LDAPException(ResultCode.DECODING_ERROR,
918 ERR_LDAPURL_HEX_STRING_TOO_SHORT.get(s));
919 }
920
921
922 final ByteBuffer byteBuffer = ByteBuffer.allocate(length - pos);
923 while (pos < length)
924 {
925 byte b;
926 switch (s.charAt(pos++))
927 {
928 case '0':
929 b = 0x00;
930 break;
931 case '1':
932 b = 0x10;
933 break;
934 case '2':
935 b = 0x20;
936 break;
937 case '3':
938 b = 0x30;
939 break;
940 case '4':
941 b = 0x40;
942 break;
943 case '5':
944 b = 0x50;
945 break;
946 case '6':
947 b = 0x60;
948 break;
949 case '7':
950 b = 0x70;
951 break;
952 case '8':
953 b = (byte) 0x80;
954 break;
955 case '9':
956 b = (byte) 0x90;
957 break;
958 case 'a':
959 case 'A':
960 b = (byte) 0xA0;
961 break;
962 case 'b':
963 case 'B':
964 b = (byte) 0xB0;
965 break;
966 case 'c':
967 case 'C':
968 b = (byte) 0xC0;
969 break;
970 case 'd':
971 case 'D':
972 b = (byte) 0xD0;
973 break;
974 case 'e':
975 case 'E':
976 b = (byte) 0xE0;
977 break;
978 case 'f':
979 case 'F':
980 b = (byte) 0xF0;
981 break;
982 default:
983 throw new LDAPException(ResultCode.DECODING_ERROR,
984 ERR_LDAPURL_INVALID_HEX_CHAR.get(
985 s.charAt(pos-1)));
986 }
987
988 if (pos >= length)
989 {
990 throw new LDAPException(ResultCode.DECODING_ERROR,
991 ERR_LDAPURL_HEX_STRING_TOO_SHORT.get(s));
992 }
993
994 switch (s.charAt(pos++))
995 {
996 case '0':
997 b |= 0x00;
998 break;
999 case '1':
1000 b |= 0x01;
1001 break;
1002 case '2':
1003 b |= 0x02;
1004 break;
1005 case '3':
1006 b |= 0x03;
1007 break;
1008 case '4':
1009 b |= 0x04;
1010 break;
1011 case '5':
1012 b |= 0x05;
1013 break;
1014 case '6':
1015 b |= 0x06;
1016 break;
1017 case '7':
1018 b |= 0x07;
1019 break;
1020 case '8':
1021 b |= 0x08;
1022 break;
1023 case '9':
1024 b |= 0x09;
1025 break;
1026 case 'a':
1027 case 'A':
1028 b |= 0x0A;
1029 break;
1030 case 'b':
1031 case 'B':
1032 b |= 0x0B;
1033 break;
1034 case 'c':
1035 case 'C':
1036 b |= 0x0C;
1037 break;
1038 case 'd':
1039 case 'D':
1040 b |= 0x0D;
1041 break;
1042 case 'e':
1043 case 'E':
1044 b |= 0x0E;
1045 break;
1046 case 'f':
1047 case 'F':
1048 b |= 0x0F;
1049 break;
1050 default:
1051 throw new LDAPException(ResultCode.DECODING_ERROR,
1052 ERR_LDAPURL_INVALID_HEX_CHAR.get(
1053 s.charAt(pos-1)));
1054 }
1055
1056 byteBuffer.put(b);
1057 if ((pos < length) && (s.charAt(pos) != '%'))
1058 {
1059 break;
1060 }
1061 }
1062
1063 byteBuffer.flip();
1064 final byte[] byteArray = new byte[byteBuffer.limit()];
1065 byteBuffer.get(byteArray);
1066
1067 buffer.append(toUTF8String(byteArray));
1068 }
1069 else
1070 {
1071 buffer.append(c);
1072 }
1073 }
1074
1075 return buffer.toString();
1076 }
1077
1078
1079
1080 /**
1081 * Appends an encoded version of the provided string to the given buffer. Any
1082 * special characters contained in the string will be replaced with byte
1083 * representations consisting of one percent sign and two hexadecimal digits
1084 * for each byte in the special character.
1085 *
1086 * @param s The string to be encoded.
1087 * @param buffer The buffer to which the encoded string will be written.
1088 */
1089 private static void percentEncode(final String s, final StringBuilder buffer)
1090 {
1091 final int length = s.length();
1092 for (int i=0; i < length; i++)
1093 {
1094 final char c = s.charAt(i);
1095
1096 switch (c)
1097 {
1098 case 'A':
1099 case 'B':
1100 case 'C':
1101 case 'D':
1102 case 'E':
1103 case 'F':
1104 case 'G':
1105 case 'H':
1106 case 'I':
1107 case 'J':
1108 case 'K':
1109 case 'L':
1110 case 'M':
1111 case 'N':
1112 case 'O':
1113 case 'P':
1114 case 'Q':
1115 case 'R':
1116 case 'S':
1117 case 'T':
1118 case 'U':
1119 case 'V':
1120 case 'W':
1121 case 'X':
1122 case 'Y':
1123 case 'Z':
1124 case 'a':
1125 case 'b':
1126 case 'c':
1127 case 'd':
1128 case 'e':
1129 case 'f':
1130 case 'g':
1131 case 'h':
1132 case 'i':
1133 case 'j':
1134 case 'k':
1135 case 'l':
1136 case 'm':
1137 case 'n':
1138 case 'o':
1139 case 'p':
1140 case 'q':
1141 case 'r':
1142 case 's':
1143 case 't':
1144 case 'u':
1145 case 'v':
1146 case 'w':
1147 case 'x':
1148 case 'y':
1149 case 'z':
1150 case '0':
1151 case '1':
1152 case '2':
1153 case '3':
1154 case '4':
1155 case '5':
1156 case '6':
1157 case '7':
1158 case '8':
1159 case '9':
1160 case '-':
1161 case '.':
1162 case '_':
1163 case '~':
1164 case '!':
1165 case '$':
1166 case '&':
1167 case '\'':
1168 case '(':
1169 case ')':
1170 case '*':
1171 case '+':
1172 case ',':
1173 case ';':
1174 case '=':
1175 buffer.append(c);
1176 break;
1177
1178 default:
1179 final byte[] charBytes = getBytes(new String(new char[] { c }));
1180 for (final byte b : charBytes)
1181 {
1182 buffer.append('%');
1183 toHex(b, buffer);
1184 }
1185 break;
1186 }
1187 }
1188 }
1189
1190
1191
1192 /**
1193 * Retrieves the scheme for this LDAP URL. It will either be "ldap", "ldaps",
1194 * or "ldapi".
1195 *
1196 * @return The scheme for this LDAP URL.
1197 */
1198 public String getScheme()
1199 {
1200 return scheme;
1201 }
1202
1203
1204
1205 /**
1206 * Retrieves the host for this LDAP URL.
1207 *
1208 * @return The host for this LDAP URL, or {@code null} if the URL does not
1209 * include a host and the client is supposed to have some external
1210 * knowledge of what the host should be.
1211 */
1212 public String getHost()
1213 {
1214 return host;
1215 }
1216
1217
1218
1219 /**
1220 * Indicates whether the URL explicitly included a host address.
1221 *
1222 * @return {@code true} if the URL explicitly included a host address, or
1223 * {@code false} if it did not.
1224 */
1225 public boolean hostProvided()
1226 {
1227 return (host != null);
1228 }
1229
1230
1231
1232 /**
1233 * Retrieves the port for this LDAP URL.
1234 *
1235 * @return The port for this LDAP URL.
1236 */
1237 public int getPort()
1238 {
1239 return port;
1240 }
1241
1242
1243
1244 /**
1245 * Indicates whether the URL explicitly included a port number.
1246 *
1247 * @return {@code true} if the URL explicitly included a port number, or
1248 * {@code false} if it did not and the default should be used.
1249 */
1250 public boolean portProvided()
1251 {
1252 return portProvided;
1253 }
1254
1255
1256
1257 /**
1258 * Retrieves the base DN for this LDAP URL.
1259 *
1260 * @return The base DN for this LDAP URL.
1261 */
1262 public DN getBaseDN()
1263 {
1264 return baseDN;
1265 }
1266
1267
1268
1269 /**
1270 * Indicates whether the URL explicitly included a base DN.
1271 *
1272 * @return {@code true} if the URL explicitly included a base DN, or
1273 * {@code false} if it did not and the default should be used.
1274 */
1275 public boolean baseDNProvided()
1276 {
1277 return baseDNProvided;
1278 }
1279
1280
1281
1282 /**
1283 * Retrieves the attribute list for this LDAP URL.
1284 *
1285 * @return The attribute list for this LDAP URL.
1286 */
1287 public String[] getAttributes()
1288 {
1289 return attributes;
1290 }
1291
1292
1293
1294 /**
1295 * Indicates whether the URL explicitly included an attribute list.
1296 *
1297 * @return {@code true} if the URL explicitly included an attribute list, or
1298 * {@code false} if it did not and the default should be used.
1299 */
1300 public boolean attributesProvided()
1301 {
1302 return attributesProvided;
1303 }
1304
1305
1306
1307 /**
1308 * Retrieves the scope for this LDAP URL.
1309 *
1310 * @return The scope for this LDAP URL.
1311 */
1312 public SearchScope getScope()
1313 {
1314 return scope;
1315 }
1316
1317
1318
1319 /**
1320 * Indicates whether the URL explicitly included a search scope.
1321 *
1322 * @return {@code true} if the URL explicitly included a search scope, or
1323 * {@code false} if it did not and the default should be used.
1324 */
1325 public boolean scopeProvided()
1326 {
1327 return scopeProvided;
1328 }
1329
1330
1331
1332 /**
1333 * Retrieves the filter for this LDAP URL.
1334 *
1335 * @return The filter for this LDAP URL.
1336 */
1337 public Filter getFilter()
1338 {
1339 return filter;
1340 }
1341
1342
1343
1344 /**
1345 * Indicates whether the URL explicitly included a search filter.
1346 *
1347 * @return {@code true} if the URL explicitly included a search filter, or
1348 * {@code false} if it did not and the default should be used.
1349 */
1350 public boolean filterProvided()
1351 {
1352 return filterProvided;
1353 }
1354
1355
1356
1357 /**
1358 * Creates a search request containing the base DN, scope, filter, and
1359 * requested attributes from this LDAP URL.
1360 *
1361 * @return The search request created from the base DN, scope, filter, and
1362 * requested attributes from this LDAP URL.
1363 */
1364 public SearchRequest toSearchRequest()
1365 {
1366 return new SearchRequest(baseDN.toString(), scope, filter, attributes);
1367 }
1368
1369
1370
1371 /**
1372 * Retrieves a hash code for this LDAP URL.
1373 *
1374 * @return A hash code for this LDAP URL.
1375 */
1376 @Override()
1377 public int hashCode()
1378 {
1379 return toNormalizedString().hashCode();
1380 }
1381
1382
1383
1384 /**
1385 * Indicates whether the provided object is equal to this LDAP URL. In order
1386 * to be considered equal, the provided object must be an LDAP URL with the
1387 * same normalized string representation.
1388 *
1389 * @param o The object for which to make the determination.
1390 *
1391 * @return {@code true} if the provided object is equal to this LDAP URL, or
1392 * {@code false} if not.
1393 */
1394 @Override()
1395 public boolean equals(final Object o)
1396 {
1397 if (o == null)
1398 {
1399 return false;
1400 }
1401
1402 if (o == this)
1403 {
1404 return true;
1405 }
1406
1407 if (! (o instanceof LDAPURL))
1408 {
1409 return false;
1410 }
1411
1412 final LDAPURL url = (LDAPURL) o;
1413 return toNormalizedString().equals(url.toNormalizedString());
1414 }
1415
1416
1417
1418 /**
1419 * Retrieves a string representation of this LDAP URL.
1420 *
1421 * @return A string representation of this LDAP URL.
1422 */
1423 @Override()
1424 public String toString()
1425 {
1426 return urlString;
1427 }
1428
1429
1430
1431 /**
1432 * Retrieves a normalized string representation of this LDAP URL.
1433 *
1434 * @return A normalized string representation of this LDAP URL.
1435 */
1436 public String toNormalizedString()
1437 {
1438 if (normalizedURLString == null)
1439 {
1440 final StringBuilder buffer = new StringBuilder();
1441 toNormalizedString(buffer);
1442 normalizedURLString = buffer.toString();
1443 }
1444
1445 return normalizedURLString;
1446 }
1447
1448
1449
1450 /**
1451 * Appends a normalized string representation of this LDAP URL to the provided
1452 * buffer.
1453 *
1454 * @param buffer The buffer to which to append the normalized string
1455 * representation of this LDAP URL.
1456 */
1457 public void toNormalizedString(final StringBuilder buffer)
1458 {
1459 buffer.append(scheme);
1460 buffer.append("://");
1461
1462 if (host != null)
1463 {
1464 if (host.indexOf(':') >= 0)
1465 {
1466 buffer.append('[');
1467 buffer.append(toLowerCase(host));
1468 buffer.append(']');
1469 }
1470 else
1471 {
1472 buffer.append(toLowerCase(host));
1473 }
1474 }
1475
1476 if (! scheme.equals("ldapi"))
1477 {
1478 buffer.append(':');
1479 buffer.append(port);
1480 }
1481
1482 buffer.append('/');
1483 percentEncode(baseDN.toNormalizedString(), buffer);
1484 buffer.append('?');
1485
1486 for (int i=0; i < attributes.length; i++)
1487 {
1488 if (i > 0)
1489 {
1490 buffer.append(',');
1491 }
1492
1493 buffer.append(toLowerCase(attributes[i]));
1494 }
1495
1496 buffer.append('?');
1497 switch (scope.intValue())
1498 {
1499 case 0: // BASE
1500 buffer.append("base");
1501 break;
1502 case 1: // ONE
1503 buffer.append("one");
1504 break;
1505 case 2: // SUB
1506 buffer.append("sub");
1507 break;
1508 case 3: // SUBORDINATE_SUBTREE
1509 buffer.append("subordinates");
1510 break;
1511 }
1512
1513 buffer.append('?');
1514 percentEncode(filter.toNormalizedString(), buffer);
1515 }
1516 }