001 /*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License"). You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at
010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012 * See the License for the specific language governing permissions
013 * and limitations under the License.
014 *
015 * When distributing Covered Code, include this CDDL HEADER in each
016 * file and include the License file at
017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
018 * add the following below this CDDL HEADER, with the fields enclosed
019 * by brackets "[]" replaced with your own identifying information:
020 * Portions Copyright [yyyy] [name of copyright owner]
021 *
022 * CDDL HEADER END
023 *
024 *
025 * Copyright 2006-2008 Sun Microsystems, Inc.
026 */
027 package org.opends.server.types;
028 import org.opends.messages.Message;
029
030
031
032 import java.util.Iterator;
033 import java.util.LinkedHashSet;
034 import java.util.LinkedList;
035 import java.util.StringTokenizer;
036
037 import org.opends.server.core.DirectoryServer;
038
039 import static org.opends.server.loggers.debug.DebugLogger.*;
040 import org.opends.server.loggers.debug.DebugTracer;
041 import static org.opends.messages.UtilityMessages.*;
042 import static org.opends.server.util.StaticUtils.*;
043
044
045
046 /**
047 * This class defines a data structure that represents the components
048 * of an LDAP URL, including the scheme, host, port, base DN,
049 * attributes, scope, filter, and extensions. It has the ability to
050 * create an LDAP URL based on all of these individual components, as
051 * well as parsing them from their string representations.
052 */
053 @org.opends.server.types.PublicAPI(
054 stability=org.opends.server.types.StabilityLevel.UNCOMMITTED,
055 mayInstantiate=true,
056 mayExtend=false,
057 mayInvoke=true)
058 public final class LDAPURL
059 {
060 /**
061 * The tracer object for the debug logger.
062 */
063 private static final DebugTracer TRACER = getTracer();
064
065 /**
066 * The default scheme that will be used if none is provided.
067 */
068 public static final String DEFAULT_SCHEME = "ldap";
069
070
071
072 /**
073 * The default port value that will be used if none is provided.
074 */
075 public static final int DEFAULT_PORT = 389;
076
077
078
079 /**
080 * The default base DN that will be used if none is provided.
081 */
082 public static final DN DEFAULT_BASE_DN = DN.nullDN();
083
084
085
086 /**
087 * The default search scope that will be used if none is provided.
088 */
089 public static final SearchScope DEFAULT_SEARCH_SCOPE =
090 SearchScope.BASE_OBJECT;
091
092
093
094 /**
095 * The default search filter that will be used if none is provided.
096 */
097 public static final SearchFilter DEFAULT_SEARCH_FILTER =
098 SearchFilter.createPresenceFilter(
099 DirectoryServer.getObjectClassAttributeType());
100
101
102
103 // The base DN for this LDAP URL.
104 private DN baseDN;
105
106 // The port number for this LDAP URL.
107 private int port;
108
109 // The set of attributes for this LDAP URL.
110 private LinkedHashSet<String> attributes;
111
112 // The set of extensions for this LDAP URL.
113 private LinkedList<String> extensions;
114
115 // The search scope for this LDAP URL.
116 private SearchScope scope;
117
118 // The search filter for this LDAP URL.
119 private SearchFilter filter;
120
121 // The host for this LDAP URL.
122 private String host;
123
124 // The raw base DN for this LDAP URL.
125 private String rawBaseDN;
126
127 // The raw filter for this LDAP URL.
128 private String rawFilter;
129
130 // The scheme (i.e., protocol) for this LDAP URL.
131 private String scheme;
132
133
134
135 /**
136 * Creates a new LDAP URL with the provided information.
137 *
138 * @param scheme The scheme (i.e., protocol) for this LDAP
139 * URL.
140 * @param host The address for this LDAP URL.
141 * @param port The port number for this LDAP URL.
142 * @param rawBaseDN The raw base DN for this LDAP URL.
143 * @param attributes The set of requested attributes for this LDAP
144 * URL.
145 * @param scope The search scope for this LDAP URL.
146 * @param rawFilter The string representation of the search
147 * filter for this LDAP URL.
148 * @param extensions The set of extensions for this LDAP URL.
149 */
150 public LDAPURL(String scheme, String host, int port,
151 String rawBaseDN, LinkedHashSet<String> attributes,
152 SearchScope scope, String rawFilter,
153 LinkedList<String> extensions)
154 {
155 this.host = toLowerCase(host);
156
157 baseDN = null;
158 filter = null;
159
160
161 if (scheme == null)
162 {
163 this.scheme = "ldap";
164 }
165 else
166 {
167 this.scheme = toLowerCase(scheme);
168 }
169
170 if ((port <= 0) || (port > 65535))
171 {
172 this.port = DEFAULT_PORT;
173 }
174 else
175 {
176 this.port = port;
177 }
178
179 if (rawBaseDN == null)
180 {
181 this.rawBaseDN = "";
182 }
183 else
184 {
185 this.rawBaseDN = rawBaseDN;
186 }
187
188 if (attributes == null)
189 {
190 this.attributes = new LinkedHashSet<String>();
191 }
192 else
193 {
194 this.attributes = attributes;
195 }
196
197 if (scope == null)
198 {
199 this.scope = DEFAULT_SEARCH_SCOPE;
200 }
201 else
202 {
203 this.scope = scope;
204 }
205
206 if (rawFilter == null)
207 {
208 this.rawFilter = "(objectClass=*)";
209 }
210 else
211 {
212 this.rawFilter = rawFilter;
213 }
214
215 if (extensions == null)
216 {
217 this.extensions = new LinkedList<String>();
218 }
219 else
220 {
221 this.extensions = extensions;
222 }
223 }
224
225
226
227 /**
228 * Creates a new LDAP URL with the provided information.
229 *
230 * @param scheme The scheme (i.e., protocol) for this LDAP
231 * URL.
232 * @param host The address for this LDAP URL.
233 * @param port The port number for this LDAP URL.
234 * @param baseDN The base DN for this LDAP URL.
235 * @param attributes The set of requested attributes for this LDAP
236 * URL.
237 * @param scope The search scope for this LDAP URL.
238 * @param filter The search filter for this LDAP URL.
239 * @param extensions The set of extensions for this LDAP URL.
240 */
241 public LDAPURL(String scheme, String host, int port, DN baseDN,
242 LinkedHashSet<String> attributes, SearchScope scope,
243 SearchFilter filter, LinkedList<String> extensions)
244 {
245 this.host = toLowerCase(host);
246
247
248 if (scheme == null)
249 {
250 this.scheme = "ldap";
251 }
252 else
253 {
254 this.scheme = toLowerCase(scheme);
255 }
256
257 if ((port <= 0) || (port > 65535))
258 {
259 this.port = DEFAULT_PORT;
260 }
261 else
262 {
263 this.port = port;
264 }
265
266 if (baseDN == null)
267 {
268 this.baseDN = DEFAULT_BASE_DN;
269 this.rawBaseDN = DEFAULT_BASE_DN.toString();
270 }
271 else
272 {
273 this.baseDN = baseDN;
274 this.rawBaseDN = baseDN.toString();
275 }
276
277 if (attributes == null)
278 {
279 this.attributes = new LinkedHashSet<String>();
280 }
281 else
282 {
283 this.attributes = attributes;
284 }
285
286 if (scope == null)
287 {
288 this.scope = DEFAULT_SEARCH_SCOPE;
289 }
290 else
291 {
292 this.scope = scope;
293 }
294
295 if (filter == null)
296 {
297 this.filter = DEFAULT_SEARCH_FILTER;
298 this.rawFilter = DEFAULT_SEARCH_FILTER.toString();
299 }
300 else
301 {
302 this.filter = filter;
303 this.rawFilter = filter.toString();
304 }
305
306 if (extensions == null)
307 {
308 this.extensions = new LinkedList<String>();
309 }
310 else
311 {
312 this.extensions = extensions;
313 }
314 }
315
316
317
318 /**
319 * Decodes the provided string as an LDAP URL.
320 *
321 * @param url The URL string to be decoded.
322 * @param fullyDecode Indicates whether the URL should be fully
323 * decoded (e.g., parsing the base DN and
324 * search filter) or just leaving them in their
325 * string representations. The latter may be
326 * required for client-side use.
327 *
328 * @return The LDAP URL decoded from the provided string.
329 *
330 * @throws DirectoryException If a problem occurs while attempting
331 * to decode the provided string as an
332 * LDAP URL.
333 */
334 public static LDAPURL decode(String url, boolean fullyDecode)
335 throws DirectoryException
336 {
337 // Find the "://" component, which will separate the scheme from
338 // the host.
339 String scheme;
340 int schemeEndPos = url.indexOf("://");
341 if (schemeEndPos < 0)
342 {
343 Message message =
344 ERR_LDAPURL_NO_COLON_SLASH_SLASH.get(String.valueOf(url));
345 throw new DirectoryException(
346 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
347 }
348 else if (schemeEndPos == 0)
349 {
350 Message message =
351 ERR_LDAPURL_NO_SCHEME.get(String.valueOf(url));
352 throw new DirectoryException(
353 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
354 }
355 else
356 {
357 scheme = urlDecode(url.substring(0, schemeEndPos));
358 }
359
360
361 // If the "://" was the end of the URL, then we're done.
362 int length = url.length();
363 if (length == schemeEndPos+3)
364 {
365 return new LDAPURL(scheme, null, DEFAULT_PORT, DEFAULT_BASE_DN,
366 null, DEFAULT_SEARCH_SCOPE,
367 DEFAULT_SEARCH_FILTER, null);
368 }
369
370
371 // Look at the next character. If it's anything but a slash, then
372 // it should be part of the host and optional port.
373 String host = null;
374 int port = DEFAULT_PORT;
375 int startPos = schemeEndPos + 3;
376 int pos = startPos;
377 while (pos < length)
378 {
379 char c = url.charAt(pos);
380 if (c == '/')
381 {
382 break;
383 }
384 else
385 {
386 pos++;
387 }
388 }
389
390 if (pos > startPos)
391 {
392 String hostPort = url.substring(startPos, pos);
393 int colonPos = hostPort.indexOf(':');
394 if (colonPos < 0)
395 {
396 host = urlDecode(hostPort);
397 }
398 else if (colonPos == 0)
399 {
400 Message message =
401 ERR_LDAPURL_NO_HOST.get(String.valueOf(url));
402 throw new DirectoryException(
403 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
404 }
405 else if (colonPos == (hostPort.length() - 1))
406 {
407 Message message =
408 ERR_LDAPURL_NO_PORT.get(String.valueOf(url));
409 throw new DirectoryException(
410 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
411 }
412 else
413 {
414 host = urlDecode(hostPort.substring(0, colonPos));
415
416 try
417 {
418 port = Integer.parseInt(hostPort.substring(colonPos+1));
419 }
420 catch (Exception e)
421 {
422 if (debugEnabled())
423 {
424 TRACER.debugCaught(DebugLogLevel.ERROR, e);
425 }
426
427 Message message = ERR_LDAPURL_CANNOT_DECODE_PORT.get(
428 String.valueOf(url), hostPort.substring(colonPos+1));
429 throw new DirectoryException(
430 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
431 }
432
433 if ((port <= 0) || (port > 65535))
434 {
435 Message message =
436 ERR_LDAPURL_INVALID_PORT.get(String.valueOf(url), port);
437 throw new DirectoryException(
438 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
439 }
440 }
441 }
442
443
444 // Move past the slash. If we're at or past the end of the
445 // string, then we're done.
446 pos++;
447 if (pos > length)
448 {
449 return new LDAPURL(scheme, host, port, DEFAULT_BASE_DN, null,
450 DEFAULT_SEARCH_SCOPE, DEFAULT_SEARCH_FILTER,
451 null);
452 }
453 else
454 {
455 startPos = pos;
456 }
457
458
459 // The next delimiter should be a question mark. If there isn't
460 // one, then the rest of the value must be the base DN.
461 String baseDNString = null;
462 pos = url.indexOf('?', startPos);
463 if (pos < 0)
464 {
465 baseDNString = url.substring(startPos);
466 startPos = length;
467 }
468 else
469 {
470 baseDNString = url.substring(startPos, pos);
471 startPos = pos+1;
472 }
473
474 DN baseDN;
475 if (fullyDecode)
476 {
477 baseDN = DN.decode(urlDecode(baseDNString));
478 }
479 else
480 {
481 baseDN = null;
482 }
483
484
485 if (startPos >= length)
486 {
487 if (fullyDecode)
488 {
489 return new LDAPURL(scheme, host, port, baseDN, null,
490 DEFAULT_SEARCH_SCOPE,
491 DEFAULT_SEARCH_FILTER, null);
492 }
493 else
494 {
495 return new LDAPURL(scheme, host, port, baseDNString, null,
496 DEFAULT_SEARCH_SCOPE, null, null);
497 }
498 }
499
500
501 // Find the next question mark (or the end of the string if there
502 // aren't any more) and get the attribute list from it.
503 String attrsString;
504 pos = url.indexOf('?', startPos);
505 if (pos < 0)
506 {
507 attrsString = url.substring(startPos);
508 startPos = length;
509 }
510 else
511 {
512 attrsString = url.substring(startPos, pos);
513 startPos = pos+1;
514 }
515
516 LinkedHashSet<String> attributes = new LinkedHashSet<String>();
517 StringTokenizer tokenizer = new StringTokenizer(attrsString, ",");
518 while (tokenizer.hasMoreTokens())
519 {
520 attributes.add(urlDecode(tokenizer.nextToken()));
521 }
522
523 if (startPos >= length)
524 {
525 if (fullyDecode)
526 {
527 return new LDAPURL(scheme, host, port, baseDN, attributes,
528 DEFAULT_SEARCH_SCOPE,
529 DEFAULT_SEARCH_FILTER, null);
530 }
531 else
532 {
533 return new LDAPURL(scheme, host, port, baseDNString,
534 attributes, DEFAULT_SEARCH_SCOPE, null,
535 null);
536 }
537 }
538
539
540 // Find the next question mark (or the end of the string if there
541 // aren't any more) and get the scope from it.
542 String scopeString;
543 pos = url.indexOf('?', startPos);
544 if (pos < 0)
545 {
546 scopeString = toLowerCase(urlDecode(url.substring(startPos)));
547 startPos = length;
548 }
549 else
550 {
551 scopeString =
552 toLowerCase(urlDecode(url.substring(startPos, pos)));
553 startPos = pos+1;
554 }
555
556 SearchScope scope;
557 if (scopeString.equals(""))
558 {
559 scope = DEFAULT_SEARCH_SCOPE;
560 }
561 else if (scopeString.equals("base"))
562 {
563 scope = SearchScope.BASE_OBJECT;
564 }
565 else if (scopeString.equals("one"))
566 {
567 scope = SearchScope.SINGLE_LEVEL;
568 }
569 else if (scopeString.equals("sub"))
570 {
571 scope = SearchScope.WHOLE_SUBTREE;
572 }
573 else if (scopeString.equals("subord") ||
574 scopeString.equals("subordinate"))
575 {
576 scope = SearchScope.SUBORDINATE_SUBTREE;
577 }
578 else
579 {
580 Message message = ERR_LDAPURL_INVALID_SCOPE_STRING.get(
581 String.valueOf(url), String.valueOf(scopeString));
582 throw new DirectoryException(
583 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
584 }
585
586 if (startPos >= length)
587 {
588 if (fullyDecode)
589 {
590 return new LDAPURL(scheme, host, port, baseDN, attributes,
591 scope, DEFAULT_SEARCH_FILTER, null);
592 }
593 else
594 {
595 return new LDAPURL(scheme, host, port, baseDNString,
596 attributes, scope, null, null);
597 }
598 }
599
600
601 // Find the next question mark (or the end of the string if there
602 // aren't any more) and get the filter from it.
603 String filterString;
604 pos = url.indexOf('?', startPos);
605 if (pos < 0)
606 {
607 filterString = urlDecode(url.substring(startPos));
608 startPos = length;
609 }
610 else
611 {
612 filterString = urlDecode(url.substring(startPos, pos));
613 startPos = pos+1;
614 }
615
616 SearchFilter filter;
617 if (fullyDecode)
618 {
619 if (filterString.equals(""))
620 {
621 filter = DEFAULT_SEARCH_FILTER;
622 }
623 else
624 {
625 filter = SearchFilter.createFilterFromString(filterString);
626 }
627
628 if (startPos >= length)
629 {
630 if (fullyDecode)
631 {
632 return new LDAPURL(scheme, host, port, baseDN, attributes,
633 scope, filter, null);
634 }
635 else
636 {
637 return new LDAPURL(scheme, host, port, baseDNString,
638 attributes, scope, filterString, null);
639 }
640 }
641 }
642 else
643 {
644 filter = null;
645 }
646
647
648 // The rest of the string must be the set of extensions.
649 String extensionsString = url.substring(startPos);
650 LinkedList<String> extensions = new LinkedList<String>();
651 tokenizer = new StringTokenizer(extensionsString, ",");
652 while (tokenizer.hasMoreTokens())
653 {
654 extensions.add(urlDecode(tokenizer.nextToken()));
655 }
656
657
658 if (fullyDecode)
659 {
660 return new LDAPURL(scheme, host, port, baseDN, attributes,
661 scope, filter, extensions);
662 }
663 else
664 {
665 return new LDAPURL(scheme, host, port, baseDNString, attributes,
666 scope, filterString, extensions);
667 }
668 }
669
670
671
672 /**
673 * Converts the provided string to a form that has decoded "special"
674 * characters that have been encoded for use in an LDAP URL.
675 *
676 * @param s The string to be decoded.
677 *
678 * @return The decoded string.
679 *
680 * @throws DirectoryException If a problem occurs while attempting
681 * to decode the contents of the
682 * provided string.
683 */
684 private static String urlDecode(String s)
685 throws DirectoryException
686 {
687 if (s == null)
688 {
689 return "";
690 }
691
692 byte[] stringBytes = getBytes(s);
693 int length = stringBytes.length;
694 byte[] decodedBytes = new byte[length];
695 int pos = 0;
696
697 for (int i=0; i < length; i++)
698 {
699 if (stringBytes[i] == '%')
700 {
701 // There must be at least two bytes left. If not, then that's
702 // a problem.
703 if (i+2 > length)
704 {
705 Message message = ERR_LDAPURL_PERCENT_TOO_CLOSE_TO_END.get(
706 String.valueOf(s), i);
707 throw new DirectoryException(
708 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
709 }
710
711 byte b;
712 switch (stringBytes[++i])
713 {
714 case '0':
715 b = (byte) 0x00;
716 break;
717 case '1':
718 b = (byte) 0x10;
719 break;
720 case '2':
721 b = (byte) 0x20;
722 break;
723 case '3':
724 b = (byte) 0x30;
725 break;
726 case '4':
727 b = (byte) 0x40;
728 break;
729 case '5':
730 b = (byte) 0x50;
731 break;
732 case '6':
733 b = (byte) 0x60;
734 break;
735 case '7':
736 b = (byte) 0x70;
737 break;
738 case '8':
739 b = (byte) 0x80;
740 break;
741 case '9':
742 b = (byte) 0x90;
743 break;
744 case 'a':
745 case 'A':
746 b = (byte) 0xA0;
747 break;
748 case 'b':
749 case 'B':
750 b = (byte) 0xB0;
751 break;
752 case 'c':
753 case 'C':
754 b = (byte) 0xC0;
755 break;
756 case 'd':
757 case 'D':
758 b = (byte) 0xD0;
759 break;
760 case 'e':
761 case 'E':
762 b = (byte) 0xE0;
763 break;
764 case 'f':
765 case 'F':
766 b = (byte) 0xF0;
767 break;
768 default:
769 Message message = ERR_LDAPURL_INVALID_HEX_BYTE.get(
770 String.valueOf(s), i);
771 throw new DirectoryException(
772 ResultCode.INVALID_ATTRIBUTE_SYNTAX,
773 message);
774 }
775
776 switch (stringBytes[++i])
777 {
778 case '0':
779 break;
780 case '1':
781 b |= 0x01;
782 break;
783 case '2':
784 b |= 0x02;
785 break;
786 case '3':
787 b |= 0x03;
788 break;
789 case '4':
790 b |= 0x04;
791 break;
792 case '5':
793 b |= 0x05;
794 break;
795 case '6':
796 b |= 0x06;
797 break;
798 case '7':
799 b |= 0x07;
800 break;
801 case '8':
802 b |= 0x08;
803 break;
804 case '9':
805 b |= 0x09;
806 break;
807 case 'a':
808 case 'A':
809 b |= 0x0A;
810 break;
811 case 'b':
812 case 'B':
813 b |= 0x0B;
814 break;
815 case 'c':
816 case 'C':
817 b |= 0x0C;
818 break;
819 case 'd':
820 case 'D':
821 b |= 0x0D;
822 break;
823 case 'e':
824 case 'E':
825 b |= 0x0E;
826 break;
827 case 'f':
828 case 'F':
829 b |= 0x0F;
830 break;
831 default:
832 Message message = ERR_LDAPURL_INVALID_HEX_BYTE.get(
833 String.valueOf(s), i);
834 throw new DirectoryException(
835 ResultCode.INVALID_ATTRIBUTE_SYNTAX,
836 message);
837 }
838
839 decodedBytes[pos++] = b;
840 }
841 else
842 {
843 decodedBytes[pos++] = stringBytes[i];
844 }
845 }
846
847 try
848 {
849 return new String(decodedBytes, 0, pos, "UTF-8");
850 }
851 catch (Exception e)
852 {
853 if (debugEnabled())
854 {
855 TRACER.debugCaught(DebugLogLevel.ERROR, e);
856 }
857
858 // This should never happen.
859 Message message = ERR_LDAPURL_CANNOT_CREATE_UTF8_STRING.get(
860 getExceptionMessage(e));
861 throw new DirectoryException(
862 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
863 }
864 }
865
866
867
868 /**
869 * Encodes the provided string portion for inclusion in an LDAP URL.
870 *
871 * @param s The string portion to be encoded.
872 * @param isExtension Indicates whether the provided component is
873 * an extension and therefore needs to have
874 * commas encoded.
875 *
876 * @return The URL-encoded version of the string portion.
877 */
878 private static String urlEncode(String s, boolean isExtension)
879 {
880 if (s == null)
881 {
882 return "";
883 }
884
885
886 int length = s.length();
887 StringBuilder buffer = new StringBuilder(length);
888 urlEncode(s, isExtension, buffer);
889
890 return buffer.toString();
891 }
892
893
894
895 /**
896 * Encodes the provided string portion for inclusion in an LDAP URL
897 * and appends it to the provided buffer.
898 *
899 * @param s The string portion to be encoded.
900 * @param isExtension Indicates whether the provided component is
901 * an extension and therefore needs to have
902 * commas encoded.
903 * @param buffer The buffer to which the information should
904 * be appended.
905 */
906 private static void urlEncode(String s, boolean isExtension,
907 StringBuilder buffer)
908 {
909 if (s == null)
910 {
911 return;
912 }
913
914 int length = s.length();
915
916 for (int i=0; i < length; i++)
917 {
918 char c = s.charAt(i);
919 if (isAlpha(c) || isDigit(c))
920 {
921 buffer.append(c);
922 continue;
923 }
924
925 if (c == ',')
926 {
927 if (isExtension)
928 {
929 hexEncode(c, buffer);
930 }
931 else
932 {
933 buffer.append(c);
934 }
935
936 continue;
937 }
938
939 switch (c)
940 {
941 case '-':
942 case '.':
943 case '_':
944 case '~':
945 case ':':
946 case '/':
947 case '#':
948 case '[':
949 case ']':
950 case '@':
951 case '!':
952 case '$':
953 case '&':
954 case '\'':
955 case '(':
956 case ')':
957 case '*':
958 case '+':
959 case ';':
960 case '=':
961 buffer.append(c);
962 break;
963 default:
964 hexEncode(c, buffer);
965 break;
966 }
967 }
968 }
969
970
971
972 /**
973 * Appends a percent-encoded representation of the provided
974 * character to the given buffer.
975 *
976 * @param c The character to add to the buffer.
977 * @param buffer The buffer to which the percent-encoded
978 * representation should be written.
979 */
980 private static void hexEncode(char c, StringBuilder buffer)
981 {
982 if ((c & (byte) 0xFF) == c)
983 {
984 // It's a single byte.
985 buffer.append('%');
986 buffer.append(byteToHex((byte) c));
987 }
988 else
989 {
990 // It requires two bytes, and each should be prefixed by a
991 // percent sign.
992 buffer.append('%');
993 byte b1 = (byte) ((c >>> 8) & 0xFF);
994 buffer.append(byteToHex(b1));
995
996 buffer.append('%');
997 byte b2 = (byte) (c & 0xFF);
998 buffer.append(byteToHex(b2));
999 }
1000 }
1001
1002
1003
1004 /**
1005 * Retrieves the scheme for this LDAP URL.
1006 *
1007 * @return The scheme for this LDAP URL.
1008 */
1009 public String getScheme()
1010 {
1011 return scheme;
1012 }
1013
1014
1015
1016 /**
1017 * Specifies the scheme for this LDAP URL.
1018 *
1019 * @param scheme The scheme for this LDAP URL.
1020 */
1021 public void setScheme(String scheme)
1022 {
1023 if (scheme == null)
1024 {
1025 this.scheme = DEFAULT_SCHEME;
1026 }
1027 else
1028 {
1029 this.scheme = scheme;
1030 }
1031 }
1032
1033
1034
1035 /**
1036 * Retrieves the host for this LDAP URL.
1037 *
1038 * @return The host for this LDAP URL, or <CODE>null</CODE> if none
1039 * was provided.
1040 */
1041 public String getHost()
1042 {
1043 return host;
1044 }
1045
1046
1047
1048 /**
1049 * Specifies the host for this LDAP URL.
1050 *
1051 * @param host The host for this LDAP URL.
1052 */
1053 public void setHost(String host)
1054 {
1055 this.host = host;
1056 }
1057
1058
1059
1060 /**
1061 * Retrieves the port for this LDAP URL.
1062 *
1063 * @return The port for this LDAP URL.
1064 */
1065 public int getPort()
1066 {
1067 return port;
1068 }
1069
1070
1071
1072 /**
1073 * Specifies the port for this LDAP URL.
1074 *
1075 * @param port The port for this LDAP URL.
1076 */
1077 public void setPort(int port)
1078 {
1079 if ((port <= 0) || (port > 65535))
1080 {
1081 this.port = DEFAULT_PORT;
1082 }
1083 else
1084 {
1085 this.port = port;
1086 }
1087 }
1088
1089
1090
1091 /**
1092 * Retrieve the raw, unprocessed base DN for this LDAP URL.
1093 *
1094 * @return The raw, unprocessed base DN for this LDAP URL, or
1095 * <CODE>null</CODE> if none was given (in which case a
1096 * default of the null DN "" should be assumed).
1097 */
1098 public String getRawBaseDN()
1099 {
1100 return rawBaseDN;
1101 }
1102
1103
1104
1105 /**
1106 * Specifies the raw, unprocessed base DN for this LDAP URL.
1107 *
1108 * @param rawBaseDN The raw, unprocessed base DN for this LDAP
1109 * URL.
1110 */
1111 public void setRawBaseDN(String rawBaseDN)
1112 {
1113 this.rawBaseDN = rawBaseDN;
1114 this.baseDN = null;
1115 }
1116
1117
1118
1119 /**
1120 * Retrieves the processed DN for this LDAP URL.
1121 *
1122 * @return The processed DN for this LDAP URL.
1123 *
1124 * @throws DirectoryException If the raw base DN cannot be decoded
1125 * as a valid DN.
1126 */
1127 public DN getBaseDN()
1128 throws DirectoryException
1129 {
1130 if (baseDN == null)
1131 {
1132 if ((rawBaseDN == null) || (rawBaseDN.length() == 0))
1133 {
1134 return DEFAULT_BASE_DN;
1135 }
1136
1137 baseDN = DN.decode(rawBaseDN);
1138 }
1139
1140 return baseDN;
1141 }
1142
1143
1144
1145 /**
1146 * Specifies the base DN for this LDAP URL.
1147 *
1148 * @param baseDN The base DN for this LDAP URL.
1149 */
1150 public void setBaseDN(DN baseDN)
1151 {
1152 if (baseDN == null)
1153 {
1154 this.baseDN = null;
1155 this.rawBaseDN = null;
1156 }
1157 else
1158 {
1159 this.baseDN = baseDN;
1160 this.rawBaseDN = baseDN.toString();
1161 }
1162 }
1163
1164
1165
1166 /**
1167 * Retrieves the set of attributes for this LDAP URL. The contents
1168 * of the returned set may be altered by the caller.
1169 *
1170 * @return The set of attributes for this LDAP URL.
1171 */
1172 public LinkedHashSet<String> getAttributes()
1173 {
1174 return attributes;
1175 }
1176
1177
1178
1179 /**
1180 * Retrieves the search scope for this LDAP URL.
1181 *
1182 * @return The search scope for this LDAP URL, or <CODE>null</CODE>
1183 * if none was given (in which case the base-level scope
1184 * should be assumed).
1185 */
1186 public SearchScope getScope()
1187 {
1188 return scope;
1189 }
1190
1191
1192
1193 /**
1194 * Specifies the search scope for this LDAP URL.
1195 *
1196 * @param scope The search scope for this LDAP URL.
1197 */
1198 public void setScope(SearchScope scope)
1199 {
1200 if (scope == null)
1201 {
1202 this.scope = DEFAULT_SEARCH_SCOPE;
1203 }
1204 else
1205 {
1206 this.scope = scope;
1207 }
1208 }
1209
1210
1211
1212 /**
1213 * Retrieves the raw, unprocessed search filter for this LDAP URL.
1214 *
1215 * @return The raw, unprocessed search filter for this LDAP URL, or
1216 * <CODE>null</CODE> if none was given (in which case a
1217 * default filter of "(objectClass=*)" should be assumed).
1218 */
1219 public String getRawFilter()
1220 {
1221 return rawFilter;
1222 }
1223
1224
1225
1226 /**
1227 * Specifies the raw, unprocessed search filter for this LDAP URL.
1228 *
1229 * @param rawFilter The raw, unprocessed search filter for this
1230 * LDAP URL.
1231 */
1232 public void setRawFilter(String rawFilter)
1233 {
1234 this.rawFilter = rawFilter;
1235 this.filter = null;
1236 }
1237
1238
1239
1240 /**
1241 * Retrieves the processed search filter for this LDAP URL.
1242 *
1243 * @return The processed search filter for this LDAP URL.
1244 *
1245 * @throws DirectoryException If a problem occurs while attempting
1246 * to decode the raw filter.
1247 */
1248 public SearchFilter getFilter()
1249 throws DirectoryException
1250 {
1251 if (filter == null)
1252 {
1253 if (rawFilter == null)
1254 {
1255 filter = DEFAULT_SEARCH_FILTER;
1256 }
1257 else
1258 {
1259 filter = SearchFilter.createFilterFromString(rawFilter);
1260 }
1261 }
1262
1263 return filter;
1264 }
1265
1266
1267
1268 /**
1269 * Specifies the search filter for this LDAP URL.
1270 *
1271 * @param filter The search filter for this LDAP URL.
1272 */
1273 public void setFilter(SearchFilter filter)
1274 {
1275 if (filter == null)
1276 {
1277 this.rawFilter = null;
1278 this.filter = null;
1279 }
1280 else
1281 {
1282 this.rawFilter = filter.toString();
1283 this.filter = filter;
1284 }
1285 }
1286
1287
1288
1289 /**
1290 * Retrieves the set of extensions for this LDAP URL. The contents
1291 * of the returned list may be altered by the caller.
1292 *
1293 * @return The set of extensions for this LDAP URL.
1294 */
1295 public LinkedList<String> getExtensions()
1296 {
1297 return extensions;
1298 }
1299
1300
1301
1302 /**
1303 * Indicates whether the provided entry matches the criteria defined
1304 * in this LDAP URL.
1305 *
1306 * @param entry The entry for which to make the determination.
1307 *
1308 * @return {@code true} if the provided entry does match the
1309 * criteria specified in this LDAP URL, or {@code false} if
1310 * it does not.
1311 *
1312 * @throws DirectoryException If a problem occurs while attempting
1313 * to make the determination.
1314 */
1315 public boolean matchesEntry(Entry entry)
1316 throws DirectoryException
1317 {
1318 SearchScope scope = getScope();
1319 if (scope == null)
1320 {
1321 scope = SearchScope.BASE_OBJECT;
1322 }
1323
1324 return (entry.matchesBaseAndScope(getBaseDN(), scope) &&
1325 getFilter().matchesEntry(entry));
1326 }
1327
1328
1329
1330 /**
1331 * Indicates whether the provided object is equal to this LDAP URL.
1332 *
1333 * @param o The object for which to make the determination.
1334 *
1335 * @return <CODE>true</CODE> if the object is equal to this LDAP
1336 * URL, or <CODE>false</CODE> if not.
1337 */
1338 public boolean equals(Object o)
1339 {
1340 if (o == null)
1341 {
1342 return false;
1343 }
1344
1345 if (o == this)
1346 {
1347 return true;
1348 }
1349
1350 if (! (o instanceof LDAPURL))
1351 {
1352 return false;
1353 }
1354
1355 LDAPURL url = (LDAPURL) o;
1356
1357 if (! scheme.equals(url.getScheme()))
1358 {
1359 return false;
1360 }
1361
1362 if (host == null)
1363 {
1364 if (url.getHost() != null)
1365 {
1366 return false;
1367 }
1368 }
1369 else
1370 {
1371 if (! host.equalsIgnoreCase(url.getHost()))
1372 {
1373 return false;
1374 }
1375 }
1376
1377 if (port != url.getPort())
1378 {
1379 return false;
1380 }
1381
1382
1383 try
1384 {
1385 DN dn = getBaseDN();
1386 if (! dn.equals(url.getBaseDN()))
1387 {
1388 return false;
1389 }
1390 }
1391 catch (Exception e)
1392 {
1393 if (debugEnabled())
1394 {
1395 TRACER.debugCaught(DebugLogLevel.ERROR, e);
1396 }
1397
1398 if (rawBaseDN == null)
1399 {
1400 if (url.getRawBaseDN() != null)
1401 {
1402 return false;
1403 }
1404 }
1405 else
1406 {
1407 if (! rawBaseDN.equals(url.getRawBaseDN()))
1408 {
1409 return false;
1410 }
1411 }
1412 }
1413
1414
1415 if (scope != url.getScope())
1416 {
1417 return false;
1418 }
1419
1420
1421 try
1422 {
1423 SearchFilter f = getFilter();
1424 if (! f.equals(url.getFilter()))
1425 {
1426 return false;
1427 }
1428 }
1429 catch (Exception e)
1430 {
1431 if (debugEnabled())
1432 {
1433 TRACER.debugCaught(DebugLogLevel.ERROR, e);
1434 }
1435
1436 if (rawFilter == null)
1437 {
1438 if (url.getRawFilter() != null)
1439 {
1440 return false;
1441 }
1442 }
1443 else
1444 {
1445 if (! rawFilter.equals(url.getRawFilter()))
1446 {
1447 return false;
1448 }
1449 }
1450 }
1451
1452
1453 if (attributes.size() != url.getAttributes().size())
1454 {
1455 return false;
1456 }
1457
1458 LinkedHashSet<String> urlAttrs = url.getAttributes();
1459 outerAttrLoop:
1460 for (String attr : attributes)
1461 {
1462 if (urlAttrs.contains(attr))
1463 {
1464 continue;
1465 }
1466
1467 for (String attr2 : urlAttrs)
1468 {
1469 if (attr.equalsIgnoreCase(attr2))
1470 {
1471 continue outerAttrLoop;
1472 }
1473 }
1474
1475 return false;
1476 }
1477
1478
1479 if (extensions.size() != url.getExtensions().size())
1480 {
1481 return false;
1482 }
1483
1484 outerExtLoop:
1485 for (String ext : extensions)
1486 {
1487 for (String urlExt : url.getExtensions())
1488 {
1489 if (ext.equals(urlExt))
1490 {
1491 continue outerExtLoop;
1492 }
1493 }
1494
1495 return false;
1496 }
1497
1498
1499 // If we've gotten here, then we'll consider them equal.
1500 return true;
1501 }
1502
1503
1504
1505 /**
1506 * Retrieves the hash code for this LDAP URL.
1507 *
1508 * @return The hash code for this LDAP URL.
1509 */
1510 public int hashCode()
1511 {
1512 int hashCode = 0;
1513
1514 hashCode += scheme.hashCode();
1515
1516 if (host != null)
1517 {
1518 hashCode += toLowerCase(host).hashCode();
1519 }
1520
1521 hashCode += port;
1522
1523 try
1524 {
1525 hashCode += getBaseDN().hashCode();
1526 }
1527 catch (Exception e)
1528 {
1529 if (debugEnabled())
1530 {
1531 TRACER.debugCaught(DebugLogLevel.ERROR, e);
1532 }
1533
1534 if (rawBaseDN != null)
1535 {
1536 hashCode += rawBaseDN.hashCode();
1537 }
1538 }
1539
1540 hashCode += getScope().intValue();
1541
1542 for (String attr : attributes)
1543 {
1544 hashCode += toLowerCase(attr).hashCode();
1545 }
1546
1547 try
1548 {
1549 hashCode += getFilter().hashCode();
1550 }
1551 catch (Exception e)
1552 {
1553 if (debugEnabled())
1554 {
1555 TRACER.debugCaught(DebugLogLevel.ERROR, e);
1556 }
1557
1558 if (rawFilter != null)
1559 {
1560 hashCode += rawFilter.hashCode();
1561 }
1562 }
1563
1564 for (String ext : extensions)
1565 {
1566 hashCode += ext.hashCode();
1567 }
1568
1569 return hashCode;
1570 }
1571
1572
1573
1574 /**
1575 * Retrieves a string representation of this LDAP URL.
1576 *
1577 * @return A string representation of this LDAP URL.
1578 */
1579 public String toString()
1580 {
1581 StringBuilder buffer = new StringBuilder();
1582 toString(buffer, false);
1583 return buffer.toString();
1584 }
1585
1586
1587
1588 /**
1589 * Appends a string representation of this LDAP URL to the provided
1590 * buffer.
1591 *
1592 * @param buffer The buffer to which the information is to be
1593 * appended.
1594 * @param baseOnly Indicates whether the resulting URL string
1595 * should only include the portion up to the base
1596 * DN, omitting the attributes, scope, filter, and
1597 * extensions.
1598 */
1599 public void toString(StringBuilder buffer, boolean baseOnly)
1600 {
1601 urlEncode(scheme, false, buffer);
1602 buffer.append("://");
1603
1604 if (host != null)
1605 {
1606 urlEncode(host, false, buffer);
1607 buffer.append(":");
1608 buffer.append(port);
1609 }
1610
1611 buffer.append("/");
1612 urlEncode(rawBaseDN, false, buffer);
1613
1614 if (baseOnly)
1615 {
1616 // If there are extensions, then we need to include them.
1617 // Technically, we only have to include critical extensions, but
1618 // we'll use all of them.
1619 if (! extensions.isEmpty())
1620 {
1621 buffer.append("????");
1622 Iterator<String> iterator = extensions.iterator();
1623 urlEncode(iterator.next(), true, buffer);
1624
1625 while (iterator.hasNext())
1626 {
1627 buffer.append(",");
1628 urlEncode(iterator.next(), true, buffer);
1629 }
1630 }
1631
1632 return;
1633 }
1634
1635 buffer.append("?");
1636 if (! attributes.isEmpty())
1637 {
1638 Iterator<String> iterator = attributes.iterator();
1639 urlEncode(iterator.next(), false, buffer);
1640
1641 while (iterator.hasNext())
1642 {
1643 buffer.append(",");
1644 urlEncode(iterator.next(), false, buffer);
1645 }
1646 }
1647
1648 buffer.append("?");
1649 switch (scope)
1650 {
1651 case BASE_OBJECT:
1652 buffer.append("base");
1653 break;
1654 case SINGLE_LEVEL:
1655 buffer.append("one");
1656 break;
1657 case WHOLE_SUBTREE:
1658 buffer.append("sub");
1659 break;
1660 case SUBORDINATE_SUBTREE:
1661 buffer.append("subordinate");
1662 break;
1663 }
1664
1665 buffer.append("?");
1666 urlEncode(rawFilter, false, buffer);
1667
1668 if (! extensions.isEmpty())
1669 {
1670 buffer.append("?");
1671 Iterator<String> iterator = extensions.iterator();
1672 urlEncode(iterator.next(), true, buffer);
1673
1674 while (iterator.hasNext())
1675 {
1676 buffer.append(",");
1677 urlEncode(iterator.next(), true, buffer);
1678 }
1679 }
1680 }
1681 }
1682