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