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