001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied. See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 *
019 */
020 package org.apache.directory.shared.ldap.util;
021
022
023 import java.io.ByteArrayOutputStream;
024 import java.io.UnsupportedEncodingException;
025 import java.net.URI;
026 import java.text.ParseException;
027 import java.util.ArrayList;
028 import java.util.HashSet;
029 import java.util.List;
030 import java.util.Set;
031
032 import org.apache.directory.shared.asn1.codec.binary.Hex;
033 import org.apache.directory.shared.i18n.I18n;
034 import org.apache.directory.shared.ldap.codec.util.HttpClientError;
035 import org.apache.directory.shared.ldap.codec.util.LdapURLEncodingException;
036 import org.apache.directory.shared.ldap.codec.util.URIException;
037 import org.apache.directory.shared.ldap.codec.util.UrlDecoderException;
038 import org.apache.directory.shared.ldap.exception.LdapInvalidDnException;
039 import org.apache.directory.shared.ldap.filter.FilterParser;
040 import org.apache.directory.shared.ldap.filter.SearchScope;
041 import org.apache.directory.shared.ldap.name.DN;
042
043
044 /**
045 * Decodes a LdapUrl, and checks that it complies with
046 * the RFC 2255. The grammar is the following :
047 * ldapurl = scheme "://" [hostport] ["/"
048 * [dn ["?" [attributes] ["?" [scope]
049 * ["?" [filter] ["?" extensions]]]]]]
050 * scheme = "ldap"
051 * attributes = attrdesc *("," attrdesc)
052 * scope = "base" / "one" / "sub"
053 * dn = DN
054 * hostport = hostport from Section 5 of RFC 1738
055 * attrdesc = AttributeDescription from Section 4.1.5 of RFC 2251
056 * filter = filter from Section 4 of RFC 2254
057 * extensions = extension *("," extension)
058 * extension = ["!"] extype ["=" exvalue]
059 * extype = token / xtoken
060 * exvalue = LDAPString
061 * token = oid from section 4.1 of RFC 2252
062 * xtoken = ("X-" / "x-") token
063 *
064 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
065 * @version $Rev: 923455 $, $Date: 2010-03-15 23:59:28 +0200 (Mon, 15 Mar 2010) $,
066 */
067 public class LdapURL
068 {
069
070 // ~ Static fields/initializers
071 // -----------------------------------------------------------------
072
073 /** The constant for "ldaps://" scheme. */
074 public static final String LDAPS_SCHEME = "ldaps://";
075
076 /** The constant for "ldap://" scheme. */
077 public static final String LDAP_SCHEME = "ldap://";
078
079 /** A null LdapURL */
080 public static final LdapURL EMPTY_URL = new LdapURL();
081
082 // ~ Instance fields
083 // ----------------------------------------------------------------------------
084
085 /** The scheme */
086 private String scheme;
087
088 /** The host */
089 private String host;
090
091 /** The port */
092 private int port;
093
094 /** The DN */
095 private DN dn;
096
097 /** The attributes */
098 private List<String> attributes;
099
100 /** The scope */
101 private SearchScope scope;
102
103 /** The filter as a string */
104 private String filter;
105
106 /** The extensions. */
107 private List<Extension> extensionList;
108
109 /** Stores the LdapURL as a String */
110 private String string;
111
112 /** Stores the LdapURL as a byte array */
113 private byte[] bytes;
114
115 /** modal parameter that forces explicit scope rendering in toString */
116 private boolean forceScopeRendering;
117
118
119 // ~ Constructors
120 // -------------------------------------------------------------------------------
121
122 /**
123 * Construct an empty LdapURL
124 */
125 public LdapURL()
126 {
127 scheme = LDAP_SCHEME;
128 host = null;
129 port = -1;
130 dn = null;
131 attributes = new ArrayList<String>();
132 scope = SearchScope.OBJECT;
133 filter = null;
134 extensionList = new ArrayList<Extension>( 2 );
135 }
136
137
138 /**
139 * Parse a LdapURL
140 * @param chars The chars containing the URL
141 * @throws LdapURLEncodingException If the URL is invalid
142 */
143 public void parse( char[] chars ) throws LdapURLEncodingException
144 {
145 scheme = LDAP_SCHEME;
146 host = null;
147 port = -1;
148 dn = null;
149 attributes = new ArrayList<String>();
150 scope = SearchScope.OBJECT;
151 filter = null;
152 extensionList = new ArrayList<Extension>( 2 );
153
154 if ( ( chars == null ) || ( chars.length == 0 ) )
155 {
156 host = "";
157 return;
158 }
159
160 // ldapurl = scheme "://" [hostport] ["/"
161 // [dn ["?" [attributes] ["?" [scope]
162 // ["?" [filter] ["?" extensions]]]]]]
163 // scheme = "ldap"
164
165 int pos = 0;
166
167 // The scheme
168 if ( ( ( pos = StringTools.areEquals( chars, 0, LDAP_SCHEME ) ) == StringTools.NOT_EQUAL )
169 && ( ( pos = StringTools.areEquals( chars, 0, LDAPS_SCHEME ) ) == StringTools.NOT_EQUAL ) )
170 {
171 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04398 ) );
172 }
173 else
174 {
175 scheme = new String( chars, 0, pos );
176 }
177
178 // The hostport
179 if ( ( pos = parseHostPort( chars, pos ) ) == -1 )
180 {
181 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04399 ) );
182 }
183
184 if ( pos == chars.length )
185 {
186 return;
187 }
188
189 // An optional '/'
190 if ( !StringTools.isCharASCII( chars, pos, '/' ) )
191 {
192 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04400, pos, chars[pos] ) );
193 }
194
195 pos++;
196
197 if ( pos == chars.length )
198 {
199 return;
200 }
201
202 // An optional DN
203 if ( ( pos = parseDN( chars, pos ) ) == -1 )
204 {
205 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04401 ) );
206 }
207
208 if ( pos == chars.length )
209 {
210 return;
211 }
212
213 // Optionals attributes
214 if ( !StringTools.isCharASCII( chars, pos, '?' ) )
215 {
216 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) );
217 }
218
219 pos++;
220
221 if ( ( pos = parseAttributes( chars, pos ) ) == -1 )
222 {
223 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04403 ) );
224 }
225
226 if ( pos == chars.length )
227 {
228 return;
229 }
230
231 // Optional scope
232 if ( !StringTools.isCharASCII( chars, pos, '?' ) )
233 {
234 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) );
235 }
236
237 pos++;
238
239 if ( ( pos = parseScope( chars, pos ) ) == -1 )
240 {
241 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04404 ) );
242 }
243
244 if ( pos == chars.length )
245 {
246 return;
247 }
248
249 // Optional filter
250 if ( !StringTools.isCharASCII( chars, pos, '?' ) )
251 {
252 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) );
253 }
254
255 pos++;
256
257 if ( pos == chars.length )
258 {
259 return;
260 }
261
262 if ( ( pos = parseFilter( chars, pos ) ) == -1 )
263 {
264 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04405 ) );
265 }
266
267 if ( pos == chars.length )
268 {
269 return;
270 }
271
272 // Optional extensions
273 if ( !StringTools.isCharASCII( chars, pos, '?' ) )
274 {
275 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) );
276 }
277
278 pos++;
279
280 if ( ( pos = parseExtensions( chars, pos ) ) == -1 )
281 {
282 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04406 ) );
283 }
284
285 if ( pos == chars.length )
286 {
287 return;
288 }
289 else
290 {
291 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04407 ) );
292 }
293 }
294
295
296 /**
297 * Create a new LdapURL from a String after having parsed it.
298 *
299 * @param string TheString that contains the LDAPURL
300 * @throws LdapURLEncodingException If the String does not comply with RFC 2255
301 */
302 public LdapURL( String string ) throws LdapURLEncodingException
303 {
304 if ( string == null )
305 {
306 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04408 ) );
307 }
308
309 try
310 {
311 bytes = string.getBytes( "UTF-8" );
312 this.string = string;
313 parse( string.toCharArray() );
314 }
315 catch ( UnsupportedEncodingException uee )
316 {
317 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04409, string ) );
318 }
319 }
320
321
322 /**
323 * Create a new LdapURL after having parsed it.
324 *
325 * @param bytes The byte buffer that contains the LDAPURL
326 * @throws LdapURLEncodingException If the byte array does not comply with RFC 2255
327 */
328 public LdapURL( byte[] bytes ) throws LdapURLEncodingException
329 {
330 if ( ( bytes == null ) || ( bytes.length == 0 ) )
331 {
332 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04410 ) );
333 }
334
335 string = StringTools.utf8ToString( bytes );
336
337 this.bytes = new byte[bytes.length];
338 System.arraycopy( bytes, 0, this.bytes, 0, bytes.length );
339
340 parse( string.toCharArray() );
341 }
342
343
344 // ~ Methods
345 // ------------------------------------------------------------------------------------
346
347 /**
348 * Parse this rule : <br>
349 * <p>
350 * <host> ::= <hostname> ':' <hostnumber><br>
351 * <hostname> ::= *[ <domainlabel> "." ] <toplabel><br>
352 * <domainlabel> ::= <alphadigit> | <alphadigit> *[
353 * <alphadigit> | "-" ] <alphadigit><br>
354 * <toplabel> ::= <alpha> | <alpha> *[ <alphadigit> |
355 * "-" ] <alphadigit><br>
356 * <hostnumber> ::= <digits> "." <digits> "."
357 * <digits> "." <digits>
358 * </p>
359 *
360 * @param chars The buffer to parse
361 * @param pos The current position in the byte buffer
362 * @return The new position in the byte buffer, or -1 if the rule does not
363 * apply to the byte buffer TODO check that the topLabel is valid
364 * (it must start with an alpha)
365 */
366 private int parseHost( char[] chars, int pos )
367 {
368
369 int start = pos;
370 boolean hadDot = false;
371 boolean hadMinus = false;
372 boolean isHostNumber = true;
373 boolean invalidIp = false;
374 int nbDots = 0;
375 int[] ipElem = new int[4];
376
377 // The host will be followed by a '/' or a ':', or by nothing if it's
378 // the end.
379 // We will search the end of the host part, and we will check some
380 // elements.
381 if ( StringTools.isCharASCII( chars, pos, '-' ) )
382 {
383
384 // We can't have a '-' on first position
385 return -1;
386 }
387
388 while ( ( pos < chars.length ) && ( chars[pos] != ':' ) && ( chars[pos] != '/' ) )
389 {
390
391 if ( StringTools.isCharASCII( chars, pos, '.' ) )
392 {
393
394 if ( ( hadMinus ) || ( hadDot ) )
395 {
396
397 // We already had a '.' just before : this is not allowed.
398 // Or we had a '-' before a '.' : ths is not allowed either.
399 return -1;
400 }
401
402 // Let's check the string we had before the dot.
403 if ( isHostNumber )
404 {
405
406 if ( nbDots < 4 )
407 {
408
409 // We had only digits. It may be an IP adress? Check it
410 if ( ipElem[nbDots] > 65535 )
411 {
412 invalidIp = true;
413 }
414 }
415 }
416
417 hadDot = true;
418 nbDots++;
419 pos++;
420 continue;
421 }
422 else
423 {
424
425 if ( hadDot && StringTools.isCharASCII( chars, pos, '-' ) )
426 {
427
428 // We can't have a '-' just after a '.'
429 return -1;
430 }
431
432 hadDot = false;
433 }
434
435 if ( StringTools.isDigit( chars, pos ) )
436 {
437
438 if ( isHostNumber && ( nbDots < 4 ) )
439 {
440 ipElem[nbDots] = ( ipElem[nbDots] * 10 ) + ( chars[pos] - '0' );
441
442 if ( ipElem[nbDots] > 65535 )
443 {
444 invalidIp = true;
445 }
446 }
447
448 hadMinus = false;
449 }
450 else if ( StringTools.isAlphaDigitMinus( chars, pos ) )
451 {
452 isHostNumber = false;
453
454 if ( StringTools.isCharASCII( chars, pos, '-' ) )
455 {
456 hadMinus = true;
457 }
458 else
459 {
460 hadMinus = false;
461 }
462 }
463 else
464 {
465 return -1;
466 }
467
468 pos++;
469 }
470
471 if ( start == pos )
472 {
473
474 // An empty host is valid
475 return pos;
476 }
477
478 // Checks the hostNumber
479 if ( isHostNumber )
480 {
481
482 // As this is a host number, we must have 3 dots.
483 if ( nbDots != 3 )
484 {
485 return -1;
486 }
487
488 if ( invalidIp )
489 {
490 return -1;
491 }
492 }
493
494 // Check if we have a '.' or a '-' in last position
495 if ( hadDot || hadMinus )
496 {
497 return -1;
498 }
499
500 host = new String( chars, start, pos - start );
501
502 return pos;
503 }
504
505
506 /**
507 * Parse this rule : <br>
508 * <p>
509 * <port> ::= <digits><br>
510 * <digits> ::= <digit> <digits-or-null><br>
511 * <digits-or-null> ::= <digit> <digits-or-null> | e<br>
512 * <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
513 * </p>
514 * The port must be between 0 and 65535.
515 *
516 * @param chars The buffer to parse
517 * @param pos The current position in the byte buffer
518 * @return The new position in the byte buffer, or -1 if the rule does not
519 * apply to the byte buffer
520 */
521 private int parsePort( char[] chars, int pos )
522 {
523
524 if ( !StringTools.isDigit( chars, pos ) )
525 {
526 return -1;
527 }
528
529 port = chars[pos] - '0';
530
531 pos++;
532
533 while ( StringTools.isDigit( chars, pos ) )
534 {
535 port = ( port * 10 ) + ( chars[pos] - '0' );
536
537 if ( port > 65535 )
538 {
539 return -1;
540 }
541
542 pos++;
543 }
544
545 return pos;
546 }
547
548
549 /**
550 * Parse this rule : <br>
551 * <p>
552 * <hostport> ::= <host> ':' <port>
553 * </p>
554 *
555 * @param chars The char array to parse
556 * @param pos The current position in the byte buffer
557 * @return The new position in the byte buffer, or -1 if the rule does not
558 * apply to the byte buffer
559 */
560 private int parseHostPort( char[] chars, int pos )
561 {
562 int hostPos = pos;
563
564 if ( ( pos = parseHost( chars, pos ) ) == -1 )
565 {
566 return -1;
567 }
568
569 // We may have a port.
570 if ( StringTools.isCharASCII( chars, pos, ':' ) )
571 {
572 if ( pos == hostPos )
573 {
574 // We should not have a port if we have no host
575 return -1;
576 }
577
578 pos++;
579 }
580 else
581 {
582 return pos;
583 }
584
585 // As we have a ':', we must have a valid port (between 0 and 65535).
586 if ( ( pos = parsePort( chars, pos ) ) == -1 )
587 {
588 return -1;
589 }
590
591 return pos;
592 }
593
594
595 /**
596 * From commons-httpclients. Converts the byte array of HTTP content
597 * characters to a string. If the specified charset is not supported,
598 * default system encoding is used.
599 *
600 * @param data the byte array to be encoded
601 * @param offset the index of the first byte to encode
602 * @param length the number of bytes to encode
603 * @param charset the desired character encoding
604 * @return The result of the conversion.
605 * @since 3.0
606 */
607 public static String getString( final byte[] data, int offset, int length, String charset )
608 {
609 if ( data == null )
610 {
611 throw new IllegalArgumentException( I18n.err( I18n.ERR_04411 ) );
612 }
613
614 if ( charset == null || charset.length() == 0 )
615 {
616 throw new IllegalArgumentException( I18n.err( I18n.ERR_04412 ) );
617 }
618
619 try
620 {
621 return new String( data, offset, length, charset );
622 }
623 catch ( UnsupportedEncodingException e )
624 {
625 return new String( data, offset, length );
626 }
627 }
628
629
630 /**
631 * From commons-httpclients. Converts the byte array of HTTP content
632 * characters to a string. If the specified charset is not supported,
633 * default system encoding is used.
634 *
635 * @param data the byte array to be encoded
636 * @param charset the desired character encoding
637 * @return The result of the conversion.
638 * @since 3.0
639 */
640 public static String getString( final byte[] data, String charset )
641 {
642 return getString( data, 0, data.length, charset );
643 }
644
645
646 /**
647 * Converts the specified string to byte array of ASCII characters.
648 *
649 * @param data the string to be encoded
650 * @return The string as a byte array.
651 * @since 3.0
652 */
653 public static byte[] getAsciiBytes( final String data )
654 {
655
656 if ( data == null )
657 {
658 throw new IllegalArgumentException( I18n.err( I18n.ERR_04411 ) );
659 }
660
661 try
662 {
663 return data.getBytes( "US-ASCII" );
664 }
665 catch ( UnsupportedEncodingException e )
666 {
667 throw new HttpClientError( I18n.err( I18n.ERR_04413 ) );
668 }
669 }
670
671
672 /**
673 * From commons-codec. Decodes an array of URL safe 7-bit characters into an
674 * array of original bytes. Escaped characters are converted back to their
675 * original representation.
676 *
677 * @param bytes array of URL safe characters
678 * @return array of original bytes
679 * @throws UrlDecoderException Thrown if URL decoding is unsuccessful
680 */
681 private static final byte[] decodeUrl( byte[] bytes ) throws UrlDecoderException
682 {
683 if ( bytes == null )
684 {
685 return StringTools.EMPTY_BYTES;
686 }
687
688 ByteArrayOutputStream buffer = new ByteArrayOutputStream();
689
690 for ( int i = 0; i < bytes.length; i++ )
691 {
692 int b = bytes[i];
693
694 if ( b == '%' )
695 {
696 try
697 {
698 int u = Character.digit( ( char ) bytes[++i], 16 );
699 int l = Character.digit( ( char ) bytes[++i], 16 );
700
701 if ( u == -1 || l == -1 )
702 {
703 throw new UrlDecoderException( I18n.err( I18n.ERR_04414 ) );
704 }
705
706 buffer.write( ( char ) ( ( u << 4 ) + l ) );
707 }
708 catch ( ArrayIndexOutOfBoundsException e )
709 {
710 throw new UrlDecoderException( I18n.err( I18n.ERR_04414 ) );
711 }
712 }
713 else
714 {
715 buffer.write( b );
716 }
717 }
718
719 return buffer.toByteArray();
720 }
721
722
723 /**
724 * From commons-httpclients. Unescape and decode a given string regarded as
725 * an escaped string with the default protocol charset.
726 *
727 * @param escaped a string
728 * @return the unescaped string
729 * @throws URIException if the string cannot be decoded (invalid)
730 * @see URI#getDefaultProtocolCharset
731 */
732 private static String decode( String escaped ) throws URIException
733 {
734 try
735 {
736 byte[] rawdata = decodeUrl( getAsciiBytes( escaped ) );
737 return getString( rawdata, "UTF-8" );
738 }
739 catch ( UrlDecoderException e )
740 {
741 throw new URIException( e.getMessage() );
742 }
743 }
744
745
746 /**
747 * Parse a string and check that it complies with RFC 2253. Here, we will
748 * just call the DN parser to do the job.
749 *
750 * @param chars The char array to be checked
751 * @param pos the starting position
752 * @return -1 if the char array does not contains a DN
753 */
754 private int parseDN( char[] chars, int pos )
755 {
756
757 int end = pos;
758
759 for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ )
760 {
761 end++;
762 }
763
764 try
765 {
766 dn = new DN( decode( new String( chars, pos, end - pos ) ) );
767 }
768 catch ( URIException ue )
769 {
770 return -1;
771 }
772 catch ( LdapInvalidDnException de )
773 {
774 return -1;
775 }
776
777 return end;
778 }
779
780
781 /**
782 * Parse the attributes part
783 *
784 * @param chars The char array to be checked
785 * @param pos the starting position
786 * @return -1 if the char array does not contains attributes
787 */
788 private int parseAttributes( char[] chars, int pos )
789 {
790
791 int start = pos;
792 int end = pos;
793 Set<String> hAttributes = new HashSet<String>();
794 boolean hadComma = false;
795
796 try
797 {
798
799 for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ )
800 {
801
802 if ( StringTools.isCharASCII( chars, i, ',' ) )
803 {
804 hadComma = true;
805
806 if ( ( end - start ) == 0 )
807 {
808
809 // An attributes must not be null
810 return -1;
811 }
812 else
813 {
814 String attribute = null;
815
816 // get the attribute. It must not be blank
817 attribute = new String( chars, start, end - start ).trim();
818
819 if ( attribute.length() == 0 )
820 {
821 return -1;
822 }
823
824 String decodedAttr = decode( attribute );
825
826 if ( !hAttributes.contains( decodedAttr ) )
827 {
828 attributes.add( decodedAttr );
829 hAttributes.add( decodedAttr );
830 }
831 }
832
833 start = i + 1;
834 }
835 else
836 {
837 hadComma = false;
838 }
839
840 end++;
841 }
842
843 if ( hadComma )
844 {
845
846 // We are not allowed to have a comma at the end of the
847 // attributes
848 return -1;
849 }
850 else
851 {
852
853 if ( end == start )
854 {
855
856 // We don't have any attributes. This is valid.
857 return end;
858 }
859
860 // Store the last attribute
861 // get the attribute. It must not be blank
862 String attribute = null;
863
864 attribute = new String( chars, start, end - start ).trim();
865
866 if ( attribute.length() == 0 )
867 {
868 return -1;
869 }
870
871 String decodedAttr = decode( attribute );
872
873 if ( !hAttributes.contains( decodedAttr ) )
874 {
875 attributes.add( decodedAttr );
876 hAttributes.add( decodedAttr );
877 }
878 }
879
880 return end;
881 }
882 catch ( URIException ue )
883 {
884 return -1;
885 }
886 }
887
888
889 /**
890 * Parse the filter part. We will use the FilterParserImpl class
891 *
892 * @param chars The char array to be checked
893 * @param pos the starting position
894 * @return -1 if the char array does not contains a filter
895 */
896 private int parseFilter( char[] chars, int pos )
897 {
898
899 int end = pos;
900
901 for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ )
902 {
903 end++;
904 }
905
906 if ( end == pos )
907 {
908 // We have no filter
909 return end;
910 }
911
912 try
913 {
914 filter = decode( new String( chars, pos, end - pos ) );
915 FilterParser.parse( filter );
916 }
917 catch ( URIException ue )
918 {
919 return -1;
920 }
921 catch ( ParseException pe )
922 {
923 return -1;
924 }
925
926 return end;
927 }
928
929
930 /**
931 * Parse the scope part.
932 *
933 * @param chars The char array to be checked
934 * @param pos the starting position
935 * @return -1 if the char array does not contains a scope
936 */
937 private int parseScope( char[] chars, int pos )
938 {
939
940 if ( StringTools.isCharASCII( chars, pos, 'b' ) || StringTools.isCharASCII( chars, pos, 'B' ) )
941 {
942 pos++;
943
944 if ( StringTools.isCharASCII( chars, pos, 'a' ) || StringTools.isCharASCII( chars, pos, 'A' ) )
945 {
946 pos++;
947
948 if ( StringTools.isCharASCII( chars, pos, 's' ) || StringTools.isCharASCII( chars, pos, 'S' ) )
949 {
950 pos++;
951
952 if ( StringTools.isCharASCII( chars, pos, 'e' ) || StringTools.isCharASCII( chars, pos, 'E' ) )
953 {
954 pos++;
955 scope = SearchScope.OBJECT;
956 return pos;
957 }
958 }
959 }
960 }
961 else if ( StringTools.isCharASCII( chars, pos, 'o' ) || StringTools.isCharASCII( chars, pos, 'O' ) )
962 {
963 pos++;
964
965 if ( StringTools.isCharASCII( chars, pos, 'n' ) || StringTools.isCharASCII( chars, pos, 'N' ) )
966 {
967 pos++;
968
969 if ( StringTools.isCharASCII( chars, pos, 'e' ) || StringTools.isCharASCII( chars, pos, 'E' ) )
970 {
971 pos++;
972
973 scope = SearchScope.ONELEVEL;
974 return pos;
975 }
976 }
977 }
978 else if ( StringTools.isCharASCII( chars, pos, 's' ) || StringTools.isCharASCII( chars, pos, 'S' ) )
979 {
980 pos++;
981
982 if ( StringTools.isCharASCII( chars, pos, 'u' ) || StringTools.isCharASCII( chars, pos, 'U' ) )
983 {
984 pos++;
985
986 if ( StringTools.isCharASCII( chars, pos, 'b' ) || StringTools.isCharASCII( chars, pos, 'B' ) )
987 {
988 pos++;
989
990 scope = SearchScope.SUBTREE;
991 return pos;
992 }
993 }
994 }
995 else if ( StringTools.isCharASCII( chars, pos, '?' ) )
996 {
997 // An empty scope. This is valid
998 return pos;
999 }
1000 else if ( pos == chars.length )
1001 {
1002 // An empty scope at the end of the URL. This is valid
1003 return pos;
1004 }
1005
1006 // The scope is not one of "one", "sub" or "base". It's an error
1007 return -1;
1008 }
1009
1010
1011 /**
1012 * Parse extensions and critical extensions.
1013 *
1014 * The grammar is :
1015 * extensions ::= extension [ ',' extension ]*
1016 * extension ::= [ '!' ] ( token | ( 'x-' | 'X-' ) token ) ) [ '=' exvalue ]
1017 *
1018 * @param chars The char array to be checked
1019 * @param pos the starting position
1020 * @return -1 if the char array does not contains valid extensions or
1021 * critical extensions
1022 */
1023 private int parseExtensions( char[] chars, int pos )
1024 {
1025 int start = pos;
1026 boolean isCritical = false;
1027 boolean isNewExtension = true;
1028 boolean hasValue = false;
1029 String extension = null;
1030 String value = null;
1031
1032 if ( pos == chars.length )
1033 {
1034 return pos;
1035 }
1036
1037 try
1038 {
1039 for ( int i = pos; ( i < chars.length ); i++ )
1040 {
1041 if ( StringTools.isCharASCII( chars, i, ',' ) )
1042 {
1043 if ( isNewExtension )
1044 {
1045 // a ',' is not allowed when we have already had one
1046 // or if we just started to parse the extensions.
1047 return -1;
1048 }
1049 else
1050 {
1051 if ( extension == null )
1052 {
1053 extension = decode( new String( chars, start, i - start ) ).trim();
1054 }
1055 else
1056 {
1057 value = decode( new String( chars, start, i - start ) ).trim();
1058 }
1059
1060 Extension ext = new Extension( isCritical, extension, value );
1061 extensionList.add( ext );
1062
1063 isNewExtension = true;
1064 hasValue = false;
1065 isCritical = false;
1066 start = i + 1;
1067 extension = null;
1068 value = null;
1069 }
1070 }
1071 else if ( StringTools.isCharASCII( chars, i, '=' ) )
1072 {
1073 if ( hasValue )
1074 {
1075 // We may have two '=' for the same extension
1076 continue;
1077 }
1078
1079 // An optionnal value
1080 extension = decode( new String( chars, start, i - start ) ).trim();
1081
1082 if ( extension.length() == 0 )
1083 {
1084 // We must have an extension
1085 return -1;
1086 }
1087
1088 hasValue = true;
1089 start = i + 1;
1090 }
1091 else if ( StringTools.isCharASCII( chars, i, '!' ) )
1092 {
1093 if ( hasValue )
1094 {
1095 // We may have two '!' in the value
1096 continue;
1097 }
1098
1099 if ( !isNewExtension )
1100 {
1101 // '!' must appears first
1102 return -1;
1103 }
1104
1105 isCritical = true;
1106 start++;
1107 }
1108 else
1109 {
1110 isNewExtension = false;
1111 }
1112 }
1113
1114 if ( extension == null )
1115 {
1116 extension = decode( new String( chars, start, chars.length - start ) ).trim();
1117 }
1118 else
1119 {
1120 value = decode( new String( chars, start, chars.length - start ) ).trim();
1121 }
1122
1123 Extension ext = new Extension( isCritical, extension, value );
1124 extensionList.add( ext );
1125
1126 return chars.length;
1127 }
1128 catch ( URIException ue )
1129 {
1130 return -1;
1131 }
1132 }
1133
1134
1135 /**
1136 * Encode a String to avoid special characters.
1137 *
1138 *
1139 * RFC 4516, section 2.1. (Percent-Encoding)
1140 *
1141 * A generated LDAP URL MUST consist only of the restricted set of
1142 * characters included in one of the following three productions defined
1143 * in [RFC3986]:
1144 *
1145 * <reserved>
1146 * <unreserved>
1147 * <pct-encoded>
1148 *
1149 * Implementations SHOULD accept other valid UTF-8 strings [RFC3629] as
1150 * input. An octet MUST be encoded using the percent-encoding mechanism
1151 * described in section 2.1 of [RFC3986] in any of these situations:
1152 *
1153 * The octet is not in the reserved set defined in section 2.2 of
1154 * [RFC3986] or in the unreserved set defined in section 2.3 of
1155 * [RFC3986].
1156 *
1157 * It is the single Reserved character '?' and occurs inside a <dn>,
1158 * <filter>, or other element of an LDAP URL.
1159 *
1160 * It is a comma character ',' that occurs inside an <exvalue>.
1161 *
1162 *
1163 * RFC 3986, section 2.2 (Reserved Characters)
1164 *
1165 * reserved = gen-delims / sub-delims
1166 * gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
1167 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
1168 * / "*" / "+" / "," / ";" / "="
1169 *
1170 *
1171 * RFC 3986, section 2.3 (Unreserved Characters)
1172 *
1173 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
1174 *
1175 *
1176 * @param url The String to encode
1177 * @param doubleEncode Set if we need to encode the comma
1178 * @return An encoded string
1179 */
1180 public static String urlEncode( String url, boolean doubleEncode )
1181 {
1182 StringBuffer sb = new StringBuffer();
1183
1184 for ( int i = 0; i < url.length(); i++ )
1185 {
1186 char c = url.charAt( i );
1187
1188 switch ( c )
1189
1190 {
1191 // reserved and unreserved characters:
1192 // just append to the buffer
1193
1194 // reserved gen-delims, excluding '?'
1195 // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
1196 case ':':
1197 case '/':
1198 case '#':
1199 case '[':
1200 case ']':
1201 case '@':
1202
1203 // reserved sub-delims, excluding ','
1204 // sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
1205 // / "*" / "+" / "," / ";" / "="
1206 case '!':
1207 case '$':
1208 case '&':
1209 case '\'':
1210 case '(':
1211 case ')':
1212 case '*':
1213 case '+':
1214 case ';':
1215 case '=':
1216
1217 // unreserved
1218 // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
1219 case 'a':
1220 case 'b':
1221 case 'c':
1222 case 'd':
1223 case 'e':
1224 case 'f':
1225 case 'g':
1226 case 'h':
1227 case 'i':
1228 case 'j':
1229 case 'k':
1230 case 'l':
1231 case 'm':
1232 case 'n':
1233 case 'o':
1234 case 'p':
1235 case 'q':
1236 case 'r':
1237 case 's':
1238 case 't':
1239 case 'u':
1240 case 'v':
1241 case 'w':
1242 case 'x':
1243 case 'y':
1244 case 'z':
1245
1246 case 'A':
1247 case 'B':
1248 case 'C':
1249 case 'D':
1250 case 'E':
1251 case 'F':
1252 case 'G':
1253 case 'H':
1254 case 'I':
1255 case 'J':
1256 case 'K':
1257 case 'L':
1258 case 'M':
1259 case 'N':
1260 case 'O':
1261 case 'P':
1262 case 'Q':
1263 case 'R':
1264 case 'S':
1265 case 'T':
1266 case 'U':
1267 case 'V':
1268 case 'W':
1269 case 'X':
1270 case 'Y':
1271 case 'Z':
1272
1273 case '0':
1274 case '1':
1275 case '2':
1276 case '3':
1277 case '4':
1278 case '5':
1279 case '6':
1280 case '7':
1281 case '8':
1282 case '9':
1283
1284 case '-':
1285 case '.':
1286 case '_':
1287 case '~':
1288
1289 sb.append( c );
1290 break;
1291
1292 case ',':
1293
1294 // special case for comma
1295 if ( doubleEncode )
1296 {
1297 sb.append( "%2c" );
1298 }
1299 else
1300 {
1301 sb.append( c );
1302 }
1303 break;
1304
1305 default:
1306
1307 // percent encoding
1308 byte[] bytes = StringTools.charToBytes( c );
1309 char[] hex = Hex.encodeHex( bytes );
1310 for ( int j = 0; j < hex.length; j++ )
1311 {
1312 if ( j % 2 == 0 )
1313 {
1314 sb.append( '%' );
1315 }
1316 sb.append( hex[j] );
1317
1318 }
1319
1320 break;
1321 }
1322 }
1323
1324 return sb.toString();
1325 }
1326
1327
1328 /**
1329 * Get a string representation of a LdapURL.
1330 *
1331 * @return A LdapURL string
1332 * @see LdapURL#forceScopeRendering
1333 */
1334 public String toString()
1335 {
1336 StringBuffer sb = new StringBuffer();
1337
1338 sb.append( scheme );
1339
1340 sb.append( ( host == null ) ? "" : host );
1341
1342 if ( port != -1 )
1343 {
1344 sb.append( ':' ).append( port );
1345 }
1346
1347 if ( dn != null )
1348 {
1349 sb.append( '/' ).append( urlEncode( dn.getName(), false ) );
1350
1351 if ( attributes.size() != 0 || forceScopeRendering
1352 || ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || ( extensionList.size() != 0 ) ) )
1353 {
1354 sb.append( '?' );
1355
1356 boolean isFirst = true;
1357
1358 for ( String attribute : attributes )
1359 {
1360 if ( isFirst )
1361 {
1362 isFirst = false;
1363 }
1364 else
1365 {
1366 sb.append( ',' );
1367 }
1368
1369 sb.append( urlEncode( attribute, false ) );
1370 }
1371 }
1372
1373 if ( forceScopeRendering )
1374 {
1375 sb.append( '?' );
1376
1377 sb.append( scope.getLdapUrlValue() );
1378 }
1379
1380 else
1381 {
1382 if ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || ( extensionList.size() != 0 ) )
1383 {
1384 sb.append( '?' );
1385
1386 switch ( scope )
1387 {
1388 case ONELEVEL:
1389 case SUBTREE:
1390 sb.append( scope.getLdapUrlValue() );
1391 break;
1392
1393 default:
1394 break;
1395 }
1396
1397 if ( ( filter != null ) || ( ( extensionList.size() != 0 ) ) )
1398 {
1399 sb.append( "?" );
1400
1401 if ( filter != null )
1402 {
1403 sb.append( urlEncode( filter, false ) );
1404 }
1405
1406 if ( ( extensionList.size() != 0 ) )
1407 {
1408 sb.append( '?' );
1409
1410 boolean isFirst = true;
1411
1412 if ( extensionList.size() != 0 )
1413 {
1414 for ( Extension extension : extensionList )
1415 {
1416 if ( !isFirst )
1417 {
1418 sb.append( ',' );
1419 }
1420 else
1421 {
1422 isFirst = false;
1423 }
1424
1425 if ( extension.isCritical )
1426 {
1427 sb.append( '!' );
1428 }
1429 sb.append( urlEncode( extension.type, false ) );
1430
1431 if ( extension.value != null )
1432 {
1433 sb.append( '=' );
1434 sb.append( urlEncode( extension.value, true ) );
1435 }
1436 }
1437 }
1438 }
1439 }
1440 }
1441 }
1442 }
1443 else
1444 {
1445 sb.append( '/' );
1446 }
1447
1448 return sb.toString();
1449 }
1450
1451
1452 /**
1453 * @return Returns the attributes.
1454 */
1455 public List<String> getAttributes()
1456 {
1457 return attributes;
1458 }
1459
1460
1461 /**
1462 * @return Returns the dn.
1463 */
1464 public DN getDn()
1465 {
1466 return dn;
1467 }
1468
1469
1470 /**
1471 * @return Returns the extensions.
1472 */
1473 public List<Extension> getExtensions()
1474 {
1475 return extensionList;
1476 }
1477
1478
1479 /**
1480 * Gets the extension.
1481 *
1482 * @param type the extension type, case-insensitive
1483 *
1484 * @return Returns the extension, null if this URL does not contain
1485 * such an extension.
1486 */
1487 public Extension getExtension( String type )
1488 {
1489 for ( Extension extension : getExtensions() )
1490 {
1491 if ( extension.getType().equalsIgnoreCase( type ) )
1492 {
1493 return extension;
1494 }
1495 }
1496 return null;
1497 }
1498
1499
1500 /**
1501 * Gets the extension value.
1502 *
1503 * @param type the extension type, case-insensitive
1504 *
1505 * @return Returns the extension value, null if this URL does not
1506 * contain such an extension or if the extension value is null.
1507 */
1508 public String getExtensionValue( String type )
1509 {
1510 for ( Extension extension : getExtensions() )
1511 {
1512 if ( extension.getType().equalsIgnoreCase( type ) )
1513 {
1514 return extension.getValue();
1515 }
1516 }
1517 return null;
1518 }
1519
1520
1521 /**
1522 * @return Returns the filter.
1523 */
1524 public String getFilter()
1525 {
1526 return filter;
1527 }
1528
1529
1530 /**
1531 * @return Returns the host.
1532 */
1533 public String getHost()
1534 {
1535 return host;
1536 }
1537
1538
1539 /**
1540 * @return Returns the port.
1541 */
1542 public int getPort()
1543 {
1544 return port;
1545 }
1546
1547
1548 /**
1549 * Returns the scope, one of {@link SearchScope.OBJECT},
1550 * {@link SearchScope.ONELEVEL} or {@link SearchScope.SUBTREE}.
1551 *
1552 * @return Returns the scope.
1553 */
1554 public SearchScope getScope()
1555 {
1556 return scope;
1557 }
1558
1559
1560 /**
1561 * @return Returns the scheme.
1562 */
1563 public String getScheme()
1564 {
1565 return scheme;
1566 }
1567
1568
1569 /**
1570 * @return the number of bytes for this LdapURL
1571 */
1572 public int getNbBytes()
1573 {
1574 return ( bytes != null ? bytes.length : 0 );
1575 }
1576
1577
1578 /**
1579 * @return a reference on the interned bytes representing this LdapURL
1580 */
1581 public byte[] getBytesReference()
1582 {
1583 return bytes;
1584 }
1585
1586
1587 /**
1588 * @return a copy of the bytes representing this LdapURL
1589 */
1590 public byte[] getBytesCopy()
1591 {
1592 if ( bytes != null )
1593 {
1594 byte[] copy = new byte[bytes.length];
1595 System.arraycopy( bytes, 0, copy, 0, bytes.length );
1596 return copy;
1597 }
1598 else
1599 {
1600 return null;
1601 }
1602 }
1603
1604
1605 /**
1606 * @return the LdapURL as a String
1607 */
1608 public String getString()
1609 {
1610 return string;
1611 }
1612
1613
1614 /**
1615 * Compute the instance's hash code
1616 * @return the instance's hash code
1617 */
1618 public int hashCode()
1619 {
1620 return this.toString().hashCode();
1621 }
1622
1623
1624 public boolean equals( Object obj )
1625 {
1626 if ( this == obj )
1627 {
1628 return true;
1629 }
1630 if ( obj == null )
1631 {
1632 return false;
1633 }
1634 if ( getClass() != obj.getClass() )
1635 {
1636 return false;
1637 }
1638
1639 final LdapURL other = ( LdapURL ) obj;
1640 return this.toString().equals( other.toString() );
1641 }
1642
1643
1644 /**
1645 * Sets the scheme. Must be "ldap://" or "ldaps://", otherwise "ldap://" is assumed as default.
1646 *
1647 * @param scheme the new scheme
1648 */
1649 public void setScheme( String scheme )
1650 {
1651 if ( scheme != null && LDAP_SCHEME.equals( scheme ) || LDAPS_SCHEME.equals( scheme ) )
1652 {
1653 this.scheme = scheme;
1654 }
1655 else
1656 {
1657 this.scheme = LDAP_SCHEME;
1658 }
1659
1660 }
1661
1662
1663 /**
1664 * Sets the host.
1665 *
1666 * @param host the new host
1667 */
1668 public void setHost( String host )
1669 {
1670 this.host = host;
1671 }
1672
1673
1674 /**
1675 * Sets the port. Must be between 1 and 65535, otherwise -1 is assumed as default.
1676 *
1677 * @param port the new port
1678 */
1679 public void setPort( int port )
1680 {
1681 if ( port < 1 || port > 65535 )
1682 {
1683 this.port = -1;
1684 }
1685 else
1686 {
1687 this.port = port;
1688 }
1689 }
1690
1691
1692 /**
1693 * Sets the dn.
1694 *
1695 * @param dn the new dn
1696 */
1697 public void setDn( DN dn )
1698 {
1699 this.dn = dn;
1700 }
1701
1702
1703 /**
1704 * Sets the attributes, null removes all existing attributes.
1705 *
1706 * @param attributes the new attributes
1707 */
1708 public void setAttributes( List<String> attributes )
1709 {
1710 if ( attributes == null )
1711 {
1712 this.attributes.clear();
1713 }
1714 else
1715 {
1716 this.attributes = attributes;
1717 }
1718 }
1719
1720
1721 /**
1722 * Sets the scope. Must be one of {@link SearchScope.OBJECT},
1723 * {@link SearchScope.ONELEVEL} or {@link SearchScope.SUBTREE},
1724 * otherwise {@link SearchScope.OBJECT} is assumed as default.
1725 *
1726 * @param scope the new scope
1727 */
1728 public void setScope( int scope )
1729 {
1730 try
1731 {
1732 this.scope = SearchScope.getSearchScope( scope );
1733 }
1734 catch ( IllegalArgumentException iae )
1735 {
1736 this.scope = SearchScope.OBJECT;
1737 }
1738 }
1739
1740
1741 /**
1742 * Sets the scope. Must be one of {@link SearchScope.OBJECT},
1743 * {@link SearchScope.ONELEVEL} or {@link SearchScope.SUBTREE},
1744 * otherwise {@link SearchScope.OBJECT} is assumed as default.
1745 *
1746 * @param scope the new scope
1747 */
1748 public void setScope( SearchScope scope )
1749 {
1750 if ( scope == null )
1751 {
1752 this.scope = SearchScope.OBJECT;
1753 }
1754 else
1755 {
1756 this.scope = scope;
1757 }
1758 }
1759
1760
1761 /**
1762 * Sets the filter.
1763 *
1764 * @param filter the new filter
1765 */
1766 public void setFilter( String filter )
1767 {
1768 this.filter = filter;
1769 }
1770
1771
1772 /**
1773 * If set to true forces the toString method to render the scope
1774 * regardless of optional nature. Use this when you want explicit
1775 * search URL scope rendering.
1776 *
1777 * @param forceScopeRendering the forceScopeRendering to set
1778 */
1779 public void setForceScopeRendering( boolean forceScopeRendering )
1780 {
1781 this.forceScopeRendering = forceScopeRendering;
1782 }
1783
1784
1785 /**
1786 * If set to true forces the toString method to render the scope
1787 * regardless of optional nature. Use this when you want explicit
1788 * search URL scope rendering.
1789 *
1790 * @return the forceScopeRendering
1791 */
1792 public boolean isForceScopeRendering()
1793 {
1794 return forceScopeRendering;
1795 }
1796
1797 /**
1798 * An inner bean to hold extension information.
1799 *
1800 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
1801 * @version $Rev: 923455 $, $Date: 2010-03-15 23:59:28 +0200 (Mon, 15 Mar 2010) $
1802 */
1803 public static class Extension
1804 {
1805 private boolean isCritical;
1806 private String type;
1807 private String value;
1808
1809
1810 /**
1811 * Creates a new instance of Extension.
1812 *
1813 * @param isCritical true for critical extension
1814 * @param type the extension type
1815 * @param value the extension value
1816 */
1817 public Extension( boolean isCritical, String type, String value )
1818 {
1819 super();
1820 this.isCritical = isCritical;
1821 this.type = type;
1822 this.value = value;
1823 }
1824
1825
1826 /**
1827 * Checks if is critical.
1828 *
1829 * @return true, if is critical
1830 */
1831 public boolean isCritical()
1832 {
1833 return isCritical;
1834 }
1835
1836
1837 /**
1838 * Sets the critical.
1839 *
1840 * @param isCritical the new critical
1841 */
1842 public void setCritical( boolean isCritical )
1843 {
1844 this.isCritical = isCritical;
1845 }
1846
1847
1848 /**
1849 * Gets the type.
1850 *
1851 * @return the type
1852 */
1853 public String getType()
1854 {
1855 return type;
1856 }
1857
1858
1859 /**
1860 * Sets the type.
1861 *
1862 * @param type the new type
1863 */
1864 public void setType( String type )
1865 {
1866 this.type = type;
1867 }
1868
1869
1870 /**
1871 * Gets the value.
1872 *
1873 * @return the value
1874 */
1875 public String getValue()
1876 {
1877 return value;
1878 }
1879
1880
1881 /**
1882 * Sets the value.
1883 *
1884 * @param value the new value
1885 */
1886 public void setValue( String value )
1887 {
1888 this.value = value;
1889 }
1890 }
1891
1892 }