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