View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   *
19   */
20  package org.apache.directory.api.ldap.model.url;
21  
22  
23  import java.io.ByteArrayOutputStream;
24  import java.io.UnsupportedEncodingException;
25  import java.text.ParseException;
26  import java.util.ArrayList;
27  import java.util.HashSet;
28  import java.util.List;
29  import java.util.Set;
30  import java.util.regex.Matcher;
31  import java.util.regex.Pattern;
32  
33  import org.apache.directory.api.i18n.I18n;
34  import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
35  import org.apache.directory.api.ldap.model.exception.LdapURLEncodingException;
36  import org.apache.directory.api.ldap.model.exception.LdapUriException;
37  import org.apache.directory.api.ldap.model.exception.UrlDecoderException;
38  import org.apache.directory.api.ldap.model.filter.FilterParser;
39  import org.apache.directory.api.ldap.model.message.SearchScope;
40  import org.apache.directory.api.ldap.model.name.Dn;
41  import org.apache.directory.api.util.Chars;
42  import org.apache.directory.api.util.StringConstants;
43  import org.apache.directory.api.util.Strings;
44  import org.apache.directory.api.util.Unicode;
45  
46  import sun.net.util.IPAddressUtil;
47  
48  
49  /**
50   * Decodes a LdapUrl, and checks that it complies with
51   * the RFC 4516. The grammar is the following :
52   * <pre>
53   * ldapurl    = scheme "://" [host [ ":" port]] ["/"
54   *                   dn ["?" [attributes] ["?" [scope]
55   *                   ["?" [filter] ["?" extensions]]]]]
56   * scheme     = "ldap"
57   * dn         = Dn
58   * attributes = attrdesc ["," attrdesc]*
59   * attrdesc   = selector ["," selector]*
60   * selector   = attributeSelector (from Section 4.5.1 of RFC4511)
61   * scope      = "base" / "one" / "sub"
62   * extensions = extension ["," extension]*
63   * extension  = ["!"] extype ["=" exvalue]
64   * extype     = oid (from Section 1.4 of RFC4512)
65   * exvalue    = LDAPString (from Section 4.1.2 of RFC4511)
66   * host       = host from Section 3.2.2 of RFC3986
67   * port       = port from Section 3.2.3 of RFC3986
68   * filter     = filter from Section 3 of RFC 4515
69   * </pre>
70   * 
71   * From Section 3.2.1/2 of RFC3986
72   * <pre>
73   * host        = IP-literal / IPv4address / reg-name
74   * port        = *DIGIT
75   * IP-literal  = "[" ( IPv6address / IPvFuture  ) "]"
76   * IPvFuture   = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
77   * IPv6address = 6( h16 ":" ) ls32 
78   *               | "::" 5( h16 ":" ) ls32
79   *               | [               h16 ] "::" 4( h16 ":" ) ls32
80   *               | [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
81   *               | [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
82   *               | [ *3( h16 ":" ) h16 ] "::"    h16 ":"   ls32
83   *               | [ *4( h16 ":" ) h16 ] "::"              ls32
84   *               | [ *5( h16 ":" ) h16 ] "::"              h16
85   *               | [ *6( h16 ":" ) h16 ] "::"
86   * IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
87   * dec-octet   = DIGIT | [1-9] DIGIT | "1" 2DIGIT | "2" [0-4] DIGIT | "25" [0-5]
88   * reg-name    = *( unreserved / pct-encoded / sub-delims )
89   * unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
90   * pct-encoded = "%" HEXDIG HEXDIG
91   * sub-delims  = "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "="
92   * h16         = 1*4HEXDIG
93   * ls32        = ( h16 ":" h16 ) / IPv4address
94   * DIGIT       = 0..9
95   * ALPHA       = A-Z / a-z
96   * HEXDIG      = DIGIT / A-F / a-f
97   * </pre>
98   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
99   */
100 public class LdapUrl
101 {
102     /** The constant for "ldaps://" scheme. */
103     public static final String LDAPS_SCHEME = "ldaps://";
104 
105     /** The constant for "ldap://" scheme. */
106     public static final String LDAP_SCHEME = "ldap://";
107 
108     /** A null LdapUrl */
109     public static final LdapUrl EMPTY_URL = new LdapUrl();
110 
111     /** The scheme */
112     private String scheme;
113 
114     /** The host */
115     private String host;
116 
117     /** The port */
118     private int port;
119 
120     /** The Dn */
121     private Dn dn;
122 
123     /** The attributes */
124     private List<String> attributes;
125 
126     /** The scope */
127     private SearchScope scope;
128 
129     /** The filter as a string */
130     private String filter;
131 
132     /** The extensions. */
133     private List<Extension> extensionList;
134 
135     /** Stores the LdapUrl as a String */
136     private String string;
137 
138     /** Stores the LdapUrl as a byte array */
139     private byte[] bytes;
140 
141     /** modal parameter that forces explicit scope rendering in toString */
142     private boolean forceScopeRendering;
143     
144     /** The type of host we use */
145     private HostTypeEnum hostType = HostTypeEnum.REGULAR_NAME;
146 
147     /** A regexp for attributes */
148     private static final Pattern ATTRIBUTE = Pattern
149         .compile( "(?:(?:\\d|[1-9]\\d*)(?:\\.(?:\\d|[1-9]\\d*))+)|(?:[a-zA-Z][a-zA-Z0-9-]*)" );
150 
151 
152     /**
153      * Construct an empty LdapUrl
154      */
155     public LdapUrl()
156     {
157         scheme = LDAP_SCHEME;
158         host = null;
159         port = -1;
160         dn = null;
161         attributes = new ArrayList<String>();
162         scope = SearchScope.OBJECT;
163         filter = null;
164         extensionList = new ArrayList<Extension>( 2 );
165     }
166 
167 
168     /**
169      * Parse a LdapUrl.
170      * 
171      * @param chars The chars containing the URL
172      * @throws org.apache.directory.api.ldap.model.exception.LdapURLEncodingException If the URL is invalid
173      */
174     private void parse( char[] chars ) throws LdapURLEncodingException
175     {
176         scheme = LDAP_SCHEME;
177         host = null;
178         port = -1;
179         dn = null;
180         attributes = new ArrayList<String>();
181         scope = SearchScope.OBJECT;
182         filter = null;
183         extensionList = new ArrayList<Extension>( 2 );
184 
185         if ( ( chars == null ) || ( chars.length == 0 ) )
186         {
187             host = "";
188             return;
189         }
190 
191         // ldapurl = scheme "://" [hostport] ["/"
192         // [dn ["?" [attributes] ["?" [scope]
193         // ["?" [filter] ["?" extensions]]]]]]
194         // scheme = "ldap"
195         int pos = 0;
196 
197         // The scheme
198         if ( ( ( pos = Strings.areEquals( chars, 0, LDAP_SCHEME ) ) == StringConstants.NOT_EQUAL )
199             && ( ( pos = Strings.areEquals( chars, 0, LDAPS_SCHEME ) ) == StringConstants.NOT_EQUAL ) )
200         {
201             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04398 ) );
202         }
203         else
204         {
205             scheme = new String( chars, 0, pos );
206         }
207 
208         // The hostport
209         if ( ( pos = parseHostPort( chars, pos ) ) == -1 )
210         {
211             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04399 ) );
212         }
213 
214         if ( pos == chars.length )
215         {
216             return;
217         }
218 
219         // An optional '/'
220         if ( !Chars.isCharASCII( chars, pos, '/' ) )
221         {
222             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04400, pos, chars[pos] ) );
223         }
224 
225         pos++;
226 
227         if ( pos == chars.length )
228         {
229             return;
230         }
231 
232         // An optional Dn
233         if ( ( pos = parseDN( chars, pos ) ) == -1 )
234         {
235             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04401 ) );
236         }
237 
238         if ( pos == chars.length )
239         {
240             return;
241         }
242 
243         // Optionals attributes
244         if ( !Chars.isCharASCII( chars, pos, '?' ) )
245         {
246             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) );
247         }
248 
249         pos++;
250 
251         if ( ( pos = parseAttributes( chars, pos ) ) == -1 )
252         {
253             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04403 ) );
254         }
255 
256         if ( pos == chars.length )
257         {
258             return;
259         }
260 
261         // Optional scope
262         if ( !Chars.isCharASCII( chars, pos, '?' ) )
263         {
264             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) );
265         }
266 
267         pos++;
268 
269         if ( ( pos = parseScope( chars, pos ) ) == -1 )
270         {
271             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04404 ) );
272         }
273 
274         if ( pos == chars.length )
275         {
276             return;
277         }
278 
279         // Optional filter
280         if ( !Chars.isCharASCII( chars, pos, '?' ) )
281         {
282             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) );
283         }
284 
285         pos++;
286 
287         if ( pos == chars.length )
288         {
289             return;
290         }
291 
292         if ( ( pos = parseFilter( chars, pos ) ) == -1 )
293         {
294             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04405 ) );
295         }
296 
297         if ( pos == chars.length )
298         {
299             return;
300         }
301 
302         // Optional extensions
303         if ( !Chars.isCharASCII( chars, pos, '?' ) )
304         {
305             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) );
306         }
307 
308         pos++;
309 
310         if ( ( pos = parseExtensions( chars, pos ) ) == -1 )
311         {
312             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04406 ) );
313         }
314 
315         if ( pos == chars.length )
316         {
317             return;
318         }
319         else
320         {
321             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04407 ) );
322         }
323     }
324 
325 
326     /**
327      * Create a new LdapUrl from a String after having parsed it.
328      *
329      * @param string TheString that contains the LdapUrl
330      * @throws LdapURLEncodingException If the String does not comply with RFC 2255
331      */
332     public LdapUrl( String string ) throws LdapURLEncodingException
333     {
334         if ( string == null )
335         {
336             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04408 ) );
337         }
338 
339         try
340         {
341             bytes = string.getBytes( "UTF-8" );
342             this.string = string;
343             parse( string.toCharArray() );
344         }
345         catch ( UnsupportedEncodingException uee )
346         {
347             throw new LdapURLEncodingException( I18n.err( I18n.ERR_04409, string ) );
348         }
349     }
350 
351 
352     /**
353      * Parse this rule : <br>
354      * <pre>
355      * host        = IP-literal / IPv4address / reg-name
356      * port        = *DIGIT
357      * <host> ::= <hostname> ':' <hostnumber><br>
358      * <hostname> ::= *[ <domainlabel> "." ] <toplabel><br>
359      * <domainlabel> ::= <alphadigit> | <alphadigit> *[
360      * <alphadigit> | "-" ] <alphadigit><br>
361      * <toplabel> ::= <alpha> | <alpha> *[ <alphadigit> |
362      * "-" ] <alphadigit><br>
363      * <hostnumber> ::= <digits> "." <digits> "."
364      * <digits> "." <digits>
365      * </pre>
366      *
367      * @param chars The buffer to parse
368      * @param pos The current position in the byte buffer
369      * @return The new position in the byte buffer, or -1 if the rule does not
370      *         apply to the byte buffer TODO check that the topLabel is valid
371      *         (it must start with an alpha)
372      */
373     private int parseHost( char[] chars, int pos )
374     {
375         int start = pos;
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         switch ( chars[pos] )
382         {
383             case '[' :
384                 // This is an IP Literal address
385                 return parseIpLiteral( chars, pos+1 );
386                 
387             case '0' :
388             case '1' :
389             case '2' :
390             case '3' :
391             case '4' :
392             case '5' :
393             case '6' :
394             case '7' :
395             case '8' :
396             case '9' :
397                 // Probably an IPV4 address, but may be a reg-name
398                 // try to parse an IPV4 address first
399                 int currentPos = parseIPV4( chars, pos );
400                 
401                 if ( currentPos != -1 )
402                 {
403                     host = new String( chars, start, currentPos - start );
404                     
405                     return currentPos;
406                 }
407                 else
408                 {
409                     //fallback to reg-name
410                 }
411                 
412             case 'a' : case 'b' : case 'c' : case 'd' : case 'e' :
413             case 'A' : case 'B' : case 'C' : case 'D' : case 'E' :
414             case 'f' : case 'g' : case 'h' : case 'i' : case 'j' :
415             case 'F' : case 'G' : case 'H' : case 'I' : case 'J' :
416             case 'k' : case 'l' : case 'm' : case 'n' : case 'o' :
417             case 'K' : case 'L' : case 'M' : case 'N' : case 'O' :
418             case 'p' : case 'q' : case 'r' : case 's' : case 't' :
419             case 'P' : case 'Q' : case 'R' : case 'S' : case 'T' :
420             case 'u' : case 'v' : case 'w' : case 'x' : case 'y' :
421             case 'U' : case 'V' : case 'W' : case 'X' : case 'Y' :
422             case 'z' : case 'Z' : case '-' : case '.' : case '_' :
423             case '~' : case '%' : case '!' : case '$' : case '&' :
424             case '\'' : case '(' : case ')' : case '*' : case '+' :
425             case ',' : case ';' : case '=' :
426                 // A reg-name
427                 return parseRegName( chars, pos );
428         }
429         
430         host = new String( chars, start, pos - start );
431 
432         return pos;
433     }
434     
435     
436     /**
437      * parse these rules :
438      * <pre>
439      * IP-literal  = "[" ( IPv6address / IPvFuture  ) "]"
440      * IPvFuture   = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
441      * IPv6address = 6( h16 ":" ) ls32 
442      *               | "::" 5( h16 ":" ) ls32
443      *               | [               h16 ] "::" 4( h16 ":" ) ls32
444      *               | [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
445      *               | [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
446      *               | [ *3( h16 ":" ) h16 ] "::"    h16 ":"   ls32
447      *               | [ *4( h16 ":" ) h16 ] "::"              ls32
448      *               | [ *5( h16 ":" ) h16 ] "::"              h16
449      *               | [ *6( h16 ":" ) h16 ] "::"
450      * h16         = 1*4HEXDIG
451      * ls32        = ( h16 ":" h16 ) / IPv4address
452      */
453     private int parseIpLiteral( char[] chars, int pos )
454     {
455         int start = pos;
456         
457         if ( Chars.isCharASCII( chars, pos, 'v' ) )
458         {
459             // This is an IPvFuture
460             pos++;
461             hostType = HostTypeEnum.IPV_FUTURE;
462             
463             pos = parseIPvFuture( chars, pos );
464             
465             if ( pos != -1 )
466             {
467                 // We don't keep the last char, which is a ']'
468                 host = new String( chars, start, pos - start - 1 );
469             }
470             
471             return pos;
472         }
473         else
474         {
475             // An IPV6 host
476             hostType = HostTypeEnum.IPV6;
477 
478             return parseIPV6( chars, pos );
479         }
480     }
481     
482     
483     /**
484      * Parse the following rules :
485      * <pre>
486      * IPv6address = 6( h16 ":" ) ls32 
487      *               | "::" 5( h16 ":" ) ls32
488      *               | [               h16 ] "::" 4( h16 ":" ) ls32
489      *               | [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
490      *               | [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
491      *               | [ *3( h16 ":" ) h16 ] "::"    h16 ":"   ls32
492      *               | [ *4( h16 ":" ) h16 ] "::"              ls32
493      *               | [ *5( h16 ":" ) h16 ] "::"              h16
494      *               | [ *6( h16 ":" ) h16 ] "::"
495      * h16         = 1*4HEXDIG
496      * ls32        = ( h16 ":" h16 ) / IPv4address
497      * </pre>
498      */
499     private int parseIPV6( char[] chars, int pos )
500     {
501         // Search for the closing ']'
502         int start = pos;
503 
504         while ( !Chars.isCharASCII( chars, pos, ']' ) )
505         {
506             pos++;
507         }
508 
509         if ( Chars.isCharASCII( chars, pos, ']' ) )
510         {
511             String hostString = new String( chars, start, pos - start );
512             
513             if ( sun.net.util.IPAddressUtil.isIPv6LiteralAddress( hostString ) )
514             {
515                 host = hostString;
516                 
517                 return  pos + 1;
518             }
519             else
520             {
521                 return -1;
522             }
523         }
524         
525         return -1;
526     }
527     
528 
529     /**
530      * Parse these rules :
531      * <pre>
532      * IPvFuture   = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
533      * </pre>
534      * (the "v" has already been parsed)
535      */
536     private int parseIPvFuture( char[] chars, int pos )
537     {
538         // We should have at least one hex digit
539         boolean hexFound = false;
540         
541         while ( Chars.isHex( chars, pos ) )
542         {
543             hexFound = true;
544             pos++;
545         }
546         
547         if ( ! hexFound )
548         {
549             return -1;
550         }
551         
552         // a dot is expected
553         if ( !Chars.isCharASCII( chars, pos, '.' ) )
554         {
555             return -1;
556         }
557         
558         // Now, we should have at least one char in unreserved / sub-delims / ":"
559         boolean valueFound = false;
560         
561         while ( !Chars.isCharASCII( chars, pos, ']' ) )
562         {
563             switch ( chars[pos] )
564             {
565                 // Unserserved
566                 // ALPHA
567                 case 'a' : case 'b' : case 'c' : case 'd' : case 'e' :
568                 case 'A' : case 'B' : case 'C' : case 'D' : case 'E' :
569                 case 'f' : case 'g' : case 'h' : case 'i' : case 'j' :
570                 case 'F' : case 'G' : case 'H' : case 'I' : case 'J' :
571                 case 'k' : case 'l' : case 'm' : case 'n' : case 'o' :
572                 case 'K' : case 'L' : case 'M' : case 'N' : case 'O' :
573                 case 'p' : case 'q' : case 'r' : case 's' : case 't' :
574                 case 'P' : case 'Q' : case 'R' : case 'S' : case 'T' :
575                 case 'u' : case 'v' : case 'w' : case 'x' : case 'y' :
576                 case 'U' : case 'V' : case 'W' : case 'X' : case 'Y' :
577                 case 'z' : case 'Z' : 
578                     
579                 // DIGITs
580                 case '0' : case '1' : case '2' : case '3' : case '4' : 
581                 case '5' : case '6' : case '7' : case '8' : case '9' :
582                     
583                 // others
584                 case '-' : case '.' : case '_' : case '~' :  
585                 
586                 // sub-delims
587                 case '!' : case '$' : case '&' : case '\'' : 
588                 case '(' : case ')' : case '*' : case '+' : case ',' : 
589                 case ';' : case '=' :
590                     
591                 // Special case for ':'
592                 case ':' :
593                     pos++;
594                     valueFound = true;
595                     break;
596                     
597                 default :
598                     // Wrong char
599                     return -1;
600             }
601         }
602         
603         if ( !valueFound )
604         {
605             return -1;
606         }
607         
608         return pos;
609     }
610     
611     
612     /**
613      * parse these rules :
614      * <pre>
615      * reg-name    = *( unreserved / pct-encoded / sub-delims )
616      * unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
617      * pct-encoded = "%" HEXDIG HEXDIG
618      * sub-delims  = "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "="
619      * HEXDIG      = DIGIT / A-F / a-f
620      * </pre>
621      */
622     private int parseRegName( char[] chars, int pos )
623     {
624         int start = pos;
625         
626         while ( !Chars.isCharASCII( chars, pos, ':' ) && !Chars.isCharASCII( chars, pos, '/' ) && ( pos < chars.length ) )
627         {
628             switch ( chars[pos] )
629             {
630                 // Unserserved
631                 // ALPHA
632                 case 'a' : case 'b' : case 'c' : case 'd' : case 'e' :
633                 case 'A' : case 'B' : case 'C' : case 'D' : case 'E' :
634                 case 'f' : case 'g' : case 'h' : case 'i' : case 'j' :
635                 case 'F' : case 'G' : case 'H' : case 'I' : case 'J' :
636                 case 'k' : case 'l' : case 'm' : case 'n' : case 'o' :
637                 case 'K' : case 'L' : case 'M' : case 'N' : case 'O' :
638                 case 'p' : case 'q' : case 'r' : case 's' : case 't' :
639                 case 'P' : case 'Q' : case 'R' : case 'S' : case 'T' :
640                 case 'u' : case 'v' : case 'w' : case 'x' : case 'y' :
641                 case 'U' : case 'V' : case 'W' : case 'X' : case 'Y' :
642                 case 'z' : case 'Z' : 
643                     
644                 // DIGITs
645                 case '0' : case '1' : case '2' : case '3' : case '4' : 
646                 case '5' : case '6' : case '7' : case '8' : case '9' :
647                     
648                 // others
649                 case '-' : case '.' : case '_' : case '~' :  
650                 
651                 // sub-delims
652                 case '!' : case '$' : case '&' : case '\'' : 
653                 case '(' : case ')' : case '*' : case '+' : case ',' : 
654                 case ';' : case '=' :
655                     pos++;
656                     break;
657                     
658                 // pct-encoded
659                 case '%' : 
660                     if ( Chars.isHex( chars, pos + 1 ) && Chars.isHex( chars, pos + 2 ) )
661                     {
662                         pos+=3;
663                     }
664                     else
665                     {
666                         return -1;
667                     }
668                     
669                 default :
670                     // Wrong char
671                     return -1;
672             }
673         }
674         
675         host = new String( chars, start, pos - start );
676         hostType = HostTypeEnum.REGULAR_NAME;
677         
678         return pos;
679     }
680 
681     
682     /**
683      * Parse these rules :
684      * <pre>
685      * IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
686      * dec-octet   = DIGIT | [1-9] DIGIT | "1" 2DIGIT | "2" [0-4] DIGIT | "25" [0-5]
687      * </pre>
688      * @param chars The buffer to parse
689      * @param pos The current position in the byte buffer
690      * 
691      * @return The new position or -1 if this is not an IPV4 address
692      */
693     private int parseIPV4( char[] chars, int pos )
694     {
695         int[] ipElem = new int[4];
696         int ipPos = pos;
697         int start = pos;
698 
699         for ( int i = 0; i < 3; i++ )
700         {
701             ipPos = parseDecOctet( chars, ipPos, ipElem, i );
702             
703             if ( ipPos == -1 )
704             {
705                 // Not an IPV4 address
706                 return -1;
707             }
708             
709             if ( chars[ipPos] != '.' )
710             {
711                 // Not an IPV4 address
712                 return -1;
713             }
714             else
715             {
716                 ipPos++;
717             }
718         }
719 
720         ipPos = parseDecOctet( chars, ipPos, ipElem, 3 );
721         
722         if ( ipPos == -1 )
723         {
724             // Not an IPV4 address
725             return -1;
726         }
727         else
728         {
729             pos = ipPos;
730             host = new String( chars, start, pos - start );
731             hostType = HostTypeEnum.IPV4;
732             
733             return pos;
734         }
735     }
736     
737     
738     /**
739      * Parse this rule :
740      * <pre>
741      * dec-octet   = DIGIT | [1-9] DIGIT | "1" 2DIGIT | "2" [0-4] DIGIT | "25" [0-5]
742      * </pre>
743      */
744     private int parseDecOctet( char[] chars, int pos, int[] ipElem, int octetNb )
745     {
746         int ipElemValue = 0;
747         boolean ipElemSeen = false;
748         boolean hasTailingZeroes = false;
749         
750         while ( Chars.isDigit( chars, pos ) )
751         {
752             ipElemSeen = true;
753             ipElemValue = ( ipElemValue * 10 ) + ( chars[pos] - '0' );
754             
755             if ( ( chars[pos] == '0' ) && hasTailingZeroes && ( ipElemValue > 0 ) )
756             {
757                 // Two 0 at the beginning : not allowed
758                 return -1;
759             }
760             
761             if ( ipElemValue > 255 )
762             {
763                 // We don't allow IPV4 address with values > 255
764                 return -1;
765             }
766 
767             pos++;
768         }
769         
770         if ( ipElemSeen )
771         {
772             ipElem[octetNb] = ipElemValue;
773             
774             return pos;
775         }
776         else
777         {
778             return -1;
779         }
780     }
781     
782 
783     /**
784      * Parse this rule : <br>
785      * <pre>
786      * <port> ::= <digit>+<br>
787      * <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
788      * </pre>
789      * The port must be between 0 and 65535.
790      *
791      * @param chars The buffer to parse
792      * @param pos The current position in the byte buffer
793      * @return The new position in the byte buffer, or -1 if the rule does not
794      *         apply to the byte buffer
795      */
796     private int parsePort( char[] chars, int pos )
797     {
798 
799         if ( !Chars.isDigit( chars, pos ) )
800         {
801             return -1;
802         }
803 
804         port = chars[pos] - '0';
805 
806         pos++;
807 
808         while ( Chars.isDigit( chars, pos ) )
809         {
810             port = ( port * 10 ) + ( chars[pos] - '0' );
811 
812             if ( port > 65535 )
813             {
814                 return -1;
815             }
816 
817             pos++;
818         }
819 
820         return pos;
821     }
822 
823 
824     /**
825      * Parse this rule : <br>
826      * <pre>
827      *   &lt;hostport> ::= &lt;host> [':' &lt;port>]
828      * </pre>
829      *
830      * @param chars The char array to parse
831      * @param pos The current position in the byte buffer
832      * @return The new position in the byte buffer, or -1 if the rule does not
833      *         apply to the byte buffer
834      */
835     private int parseHostPort( char[] chars, int pos )
836     {
837         int hostPos = pos;
838 
839         if ( ( pos = parseHost( chars, pos ) ) == -1 )
840         {
841             return -1;
842         }
843 
844         // We may have a port.
845         if ( Chars.isCharASCII( chars, pos, ':' ) )
846         {
847             if ( pos == hostPos )
848             {
849                 // We should not have a port if we have no host
850                 return -1;
851             }
852 
853             pos++;
854         }
855         else
856         {
857             return pos;
858         }
859 
860         // As we have a ':', we must have a valid port (between 0 and 65535).
861         if ( ( pos = parsePort( chars, pos ) ) == -1 )
862         {
863             return -1;
864         }
865 
866         return pos;
867     }
868 
869 
870     /**
871      * Converts the specified string to byte array of ASCII characters.
872      *
873      * @param data the string to be encoded
874      * @return The string as a byte array.
875      * @throws org.apache.directory.api.ldap.model.exception.UrlDecoderException if encoding is not supported
876      */
877     private static byte[] getAsciiBytes( final String data ) throws UrlDecoderException
878     {
879 
880         if ( data == null )
881         {
882             throw new IllegalArgumentException( I18n.err( I18n.ERR_04411 ) );
883         }
884 
885         try
886         {
887             return data.getBytes( "US-ASCII" );
888         }
889         catch ( UnsupportedEncodingException e )
890         {
891             throw new UrlDecoderException( I18n.err( I18n.ERR_04413 ) );
892         }
893     }
894 
895 
896     /**
897      * From commons-codec. Decodes an array of URL safe 7-bit characters into an
898      * array of original bytes. Escaped characters are converted back to their
899      * original representation.
900      *
901      * @param bytes array of URL safe characters
902      * @return array of original bytes
903      * @throws UrlDecoderException Thrown if URL decoding is unsuccessful
904      */
905     private static byte[] decodeUrl( byte[] bytes ) throws UrlDecoderException
906     {
907         if ( bytes == null )
908         {
909             return StringConstants.EMPTY_BYTES;
910         }
911 
912         ByteArrayOutputStream buffer = new ByteArrayOutputStream();
913 
914         for ( int i = 0; i < bytes.length; i++ )
915         {
916             int b = bytes[i];
917 
918             if ( b == '%' )
919             {
920                 try
921                 {
922                     int u = Character.digit( ( char ) bytes[++i], 16 );
923                     int l = Character.digit( ( char ) bytes[++i], 16 );
924 
925                     if ( ( u == -1 ) || ( l == -1 ) )
926                     {
927                         throw new UrlDecoderException( I18n.err( I18n.ERR_04414 ) );
928                     }
929 
930                     buffer.write( ( char ) ( ( u << 4 ) + l ) );
931                 }
932                 catch ( ArrayIndexOutOfBoundsException e )
933                 {
934                     throw new UrlDecoderException( I18n.err( I18n.ERR_04414 ) );
935                 }
936             }
937             else
938             {
939                 buffer.write( b );
940             }
941         }
942 
943         return buffer.toByteArray();
944     }
945 
946 
947     /**
948      * From commons-httpclients. Unescape and decode a given string regarded as
949      * an escaped string with the default protocol charset.
950      *
951      * @param escaped a string
952      * @return the unescaped string
953      * @throws LdapUriException if the string cannot be decoded (invalid)
954      */
955     private static String decode( String escaped ) throws LdapUriException
956     {
957         try
958         {
959             byte[] rawdata = decodeUrl( getAsciiBytes( escaped ) );
960             return Strings.getString( rawdata, "UTF-8" );
961         }
962         catch ( UrlDecoderException e )
963         {
964             throw new LdapUriException( e.getMessage(), e );
965         }
966     }
967 
968 
969     /**
970      * Parse a string and check that it complies with RFC 2253. Here, we will
971      * just call the Dn parser to do the job.
972      *
973      * @param chars The char array to be checked
974      * @param pos the starting position
975      * @return -1 if the char array does not contains a Dn
976      */
977     private int parseDN( char[] chars, int pos )
978     {
979 
980         int end = pos;
981 
982         for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ )
983         {
984             end++;
985         }
986 
987         try
988         {
989             String dnStr = new String( chars, pos, end - pos );
990             dn = new Dn( decode( dnStr ) );
991         }
992         catch ( LdapUriException ue )
993         {
994             return -1;
995         }
996         catch ( LdapInvalidDnException de )
997         {
998             return -1;
999         }
1000 
1001         return end;
1002     }
1003 
1004 
1005     /**
1006      * Parse the following rule :
1007      * <pre>
1008      * oid ::= numericOid | descr
1009      * descr ::= keystring
1010      * keystring ::= leadkeychar *keychar
1011      * leadkeychar ::= [a-zA-Z]
1012      * keychar ::= [a-zA-Z0-0-]
1013      * numericOid ::= number 1*( DOT number )
1014      * number ::= 0 | [1-9][0-9]* 
1015      * 
1016      * @param attribute
1017      * @throws LdapURLEncodingException
1018      */
1019     private void validateAttribute( String attribute ) throws LdapURLEncodingException
1020     {
1021         Matcher matcher = ATTRIBUTE.matcher( attribute );
1022 
1023         if ( !matcher.matches() )
1024         {
1025             throw new LdapURLEncodingException( "Attribute " + attribute + " is invalid" );
1026         }
1027     }
1028 
1029 
1030     /**
1031      * Parse the attributes part
1032      *
1033      * @param chars The char array to be checked
1034      * @param pos the starting position
1035      * @return -1 if the char array does not contains attributes
1036      */
1037     private int parseAttributes( char[] chars, int pos )
1038     {
1039         int start = pos;
1040         int end = pos;
1041         Set<String> hAttributes = new HashSet<String>();
1042         boolean hadComma = false;
1043 
1044         try
1045         {
1046 
1047             for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ )
1048             {
1049 
1050                 if ( Chars.isCharASCII( chars, i, ',' ) )
1051                 {
1052                     hadComma = true;
1053 
1054                     if ( ( end - start ) == 0 )
1055                     {
1056 
1057                         // An attributes must not be null
1058                         return -1;
1059                     }
1060                     else
1061                     {
1062                         String attribute = null;
1063 
1064                         // get the attribute. It must not be blank
1065                         attribute = new String( chars, start, end - start ).trim();
1066 
1067                         if ( attribute.length() == 0 )
1068                         {
1069                             return -1;
1070                         }
1071 
1072                         // Check that the attribute is valid
1073                         try
1074                         {
1075                             validateAttribute( attribute );
1076                         }
1077                         catch ( LdapURLEncodingException luee )
1078                         {
1079                             return -1;
1080                         }
1081 
1082                         String decodedAttr = decode( attribute );
1083 
1084                         if ( !hAttributes.contains( decodedAttr ) )
1085                         {
1086                             attributes.add( decodedAttr );
1087                             hAttributes.add( decodedAttr );
1088                         }
1089                     }
1090 
1091                     start = i + 1;
1092                 }
1093                 else
1094                 {
1095                     hadComma = false;
1096                 }
1097 
1098                 end++;
1099             }
1100 
1101             if ( hadComma )
1102             {
1103 
1104                 // We are not allowed to have a comma at the end of the
1105                 // attributes
1106                 return -1;
1107             }
1108             else
1109             {
1110 
1111                 if ( end == start )
1112                 {
1113 
1114                     // We don't have any attributes. This is valid.
1115                     return end;
1116                 }
1117 
1118                 // Store the last attribute
1119                 // get the attribute. It must not be blank
1120                 String attribute = null;
1121 
1122                 attribute = new String( chars, start, end - start ).trim();
1123 
1124                 if ( attribute.length() == 0 )
1125                 {
1126                     return -1;
1127                 }
1128 
1129                 String decodedAttr = decode( attribute );
1130 
1131                 if ( !hAttributes.contains( decodedAttr ) )
1132                 {
1133                     attributes.add( decodedAttr );
1134                     hAttributes.add( decodedAttr );
1135                 }
1136             }
1137 
1138             return end;
1139         }
1140         catch ( LdapUriException ue )
1141         {
1142             return -1;
1143         }
1144     }
1145 
1146 
1147     /**
1148      * Parse the filter part. We will use the FilterParserImpl class
1149      *
1150      * @param chars The char array to be checked
1151      * @param pos the starting position
1152      * @return -1 if the char array does not contains a filter
1153      */
1154     private int parseFilter( char[] chars, int pos )
1155     {
1156 
1157         int end = pos;
1158 
1159         for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ )
1160         {
1161             end++;
1162         }
1163 
1164         if ( end == pos )
1165         {
1166             // We have no filter
1167             return end;
1168         }
1169 
1170         try
1171         {
1172             filter = decode( new String( chars, pos, end - pos ) );
1173             FilterParser.parse( null, filter );
1174         }
1175         catch ( LdapUriException ue )
1176         {
1177             return -1;
1178         }
1179         catch ( ParseException pe )
1180         {
1181             return -1;
1182         }
1183 
1184         return end;
1185     }
1186 
1187 
1188     /**
1189      * Parse the scope part.
1190      *
1191      * @param chars The char array to be checked
1192      * @param pos the starting position
1193      * @return -1 if the char array does not contains a scope
1194      */
1195     private int parseScope( char[] chars, int pos )
1196     {
1197 
1198         if ( Chars.isCharASCII( chars, pos, 'b' ) || Chars.isCharASCII( chars, pos, 'B' ) )
1199         {
1200             pos++;
1201 
1202             if ( Chars.isCharASCII( chars, pos, 'a' ) || Chars.isCharASCII( chars, pos, 'A' ) )
1203             {
1204                 pos++;
1205 
1206                 if ( Chars.isCharASCII( chars, pos, 's' ) || Chars.isCharASCII( chars, pos, 'S' ) )
1207                 {
1208                     pos++;
1209 
1210                     if ( Chars.isCharASCII( chars, pos, 'e' ) || Chars.isCharASCII( chars, pos, 'E' ) )
1211                     {
1212                         pos++;
1213                         scope = SearchScope.OBJECT;
1214                         return pos;
1215                     }
1216                 }
1217             }
1218         }
1219         else if ( Chars.isCharASCII( chars, pos, 'o' ) || Chars.isCharASCII( chars, pos, 'O' ) )
1220         {
1221             pos++;
1222 
1223             if ( Chars.isCharASCII( chars, pos, 'n' ) || Chars.isCharASCII( chars, pos, 'N' ) )
1224             {
1225                 pos++;
1226 
1227                 if ( Chars.isCharASCII( chars, pos, 'e' ) || Chars.isCharASCII( chars, pos, 'E' ) )
1228                 {
1229                     pos++;
1230 
1231                     scope = SearchScope.ONELEVEL;
1232                     return pos;
1233                 }
1234             }
1235         }
1236         else if ( Chars.isCharASCII( chars, pos, 's' ) || Chars.isCharASCII( chars, pos, 'S' ) )
1237         {
1238             pos++;
1239 
1240             if ( Chars.isCharASCII( chars, pos, 'u' ) || Chars.isCharASCII( chars, pos, 'U' ) )
1241             {
1242                 pos++;
1243 
1244                 if ( Chars.isCharASCII( chars, pos, 'b' ) || Chars.isCharASCII( chars, pos, 'B' ) )
1245                 {
1246                     pos++;
1247 
1248                     scope = SearchScope.SUBTREE;
1249                     return pos;
1250                 }
1251             }
1252         }
1253         else if ( Chars.isCharASCII( chars, pos, '?' ) )
1254         {
1255             // An empty scope. This is valid
1256             return pos;
1257         }
1258         else if ( pos == chars.length )
1259         {
1260             // An empty scope at the end of the URL. This is valid
1261             return pos;
1262         }
1263 
1264         // The scope is not one of "one", "sub" or "base". It's an error
1265         return -1;
1266     }
1267 
1268 
1269     /**
1270      * Parse extensions and critical extensions.
1271      *
1272      * The grammar is :
1273      * extensions ::= extension [ ',' extension ]*
1274      * extension ::= [ '!' ] ( token | ( 'x-' | 'X-' ) token ) ) [ '=' exvalue ]
1275      *
1276      * @param chars The char array to be checked
1277      * @param pos the starting position
1278      * @return -1 if the char array does not contains valid extensions or
1279      *         critical extensions
1280      */
1281     private int parseExtensions( char[] chars, int pos )
1282     {
1283         int start = pos;
1284         boolean isCritical = false;
1285         boolean isNewExtension = true;
1286         boolean hasValue = false;
1287         String extension = null;
1288         String value = null;
1289 
1290         if ( pos == chars.length )
1291         {
1292             return pos;
1293         }
1294 
1295         try
1296         {
1297             for ( int i = pos; ( i < chars.length ); i++ )
1298             {
1299                 if ( Chars.isCharASCII( chars, i, ',' ) )
1300                 {
1301                     if ( isNewExtension )
1302                     {
1303                         // a ',' is not allowed when we have already had one
1304                         // or if we just started to parse the extensions.
1305                         return -1;
1306                     }
1307                     else
1308                     {
1309                         if ( extension == null )
1310                         {
1311                             extension = decode( new String( chars, start, i - start ) ).trim();
1312                         }
1313                         else
1314                         {
1315                             value = decode( new String( chars, start, i - start ) ).trim();
1316                         }
1317 
1318                         Extension ext = new Extension( isCritical, extension, value );
1319                         extensionList.add( ext );
1320 
1321                         isNewExtension = true;
1322                         hasValue = false;
1323                         isCritical = false;
1324                         start = i + 1;
1325                         extension = null;
1326                         value = null;
1327                     }
1328                 }
1329                 else if ( Chars.isCharASCII( chars, i, '=' ) )
1330                 {
1331                     if ( hasValue )
1332                     {
1333                         // We may have two '=' for the same extension
1334                         continue;
1335                     }
1336 
1337                     // An optionnal value
1338                     extension = decode( new String( chars, start, i - start ) ).trim();
1339 
1340                     if ( extension.length() == 0 )
1341                     {
1342                         // We must have an extension
1343                         return -1;
1344                     }
1345 
1346                     hasValue = true;
1347                     start = i + 1;
1348                 }
1349                 else if ( Chars.isCharASCII( chars, i, '!' ) )
1350                 {
1351                     if ( hasValue )
1352                     {
1353                         // We may have two '!' in the value
1354                         continue;
1355                     }
1356 
1357                     if ( !isNewExtension )
1358                     {
1359                         // '!' must appears first
1360                         return -1;
1361                     }
1362 
1363                     isCritical = true;
1364                     start++;
1365                 }
1366                 else
1367                 {
1368                     isNewExtension = false;
1369                 }
1370             }
1371 
1372             if ( extension == null )
1373             {
1374                 extension = decode( new String( chars, start, chars.length - start ) ).trim();
1375             }
1376             else
1377             {
1378                 value = decode( new String( chars, start, chars.length - start ) ).trim();
1379             }
1380 
1381             Extension ext = new Extension( isCritical, extension, value );
1382             extensionList.add( ext );
1383 
1384             return chars.length;
1385         }
1386         catch ( LdapUriException ue )
1387         {
1388             return -1;
1389         }
1390     }
1391 
1392 
1393     /**
1394      * Encode a String to avoid special characters.
1395      *
1396      *
1397      * RFC 4516, section 2.1. (Percent-Encoding)
1398      *
1399      * A generated LDAP URL MUST consist only of the restricted set of
1400      * characters included in one of the following three productions defined
1401      * in [RFC3986]:
1402      *
1403      *   <reserved>
1404      *   <unreserved>
1405      *   <pct-encoded>
1406      *
1407      * Implementations SHOULD accept other valid UTF-8 strings [RFC3629] as
1408      * input.  An octet MUST be encoded using the percent-encoding mechanism
1409      * described in section 2.1 of [RFC3986] in any of these situations:
1410      *
1411      *  The octet is not in the reserved set defined in section 2.2 of
1412      *  [RFC3986] or in the unreserved set defined in section 2.3 of
1413      *  [RFC3986].
1414      *
1415      *  It is the single Reserved character '?' and occurs inside a <dn>,
1416      *  <filter>, or other element of an LDAP URL.
1417      *
1418      *  It is a comma character ',' that occurs inside an <exvalue>.
1419      *
1420      *
1421      * RFC 3986, section 2.2 (Reserved Characters)
1422      *
1423      * reserved    = gen-delims / sub-delims
1424      * gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
1425      * sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
1426      *              / "*" / "+" / "," / ";" / "="
1427      *
1428      *
1429      * RFC 3986, section 2.3 (Unreserved Characters)
1430      *
1431      * unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
1432      *
1433      *
1434      * @param url The String to encode
1435      * @param doubleEncode Set if we need to encode the comma
1436      * @return An encoded string
1437      */
1438     public static String urlEncode( String url, boolean doubleEncode )
1439     {
1440         StringBuffer sb = new StringBuffer();
1441 
1442         for ( int i = 0; i < url.length(); i++ )
1443         {
1444             char c = url.charAt( i );
1445 
1446             switch ( c )
1447 
1448             {
1449             // reserved and unreserved characters:
1450             // just append to the buffer
1451 
1452             // reserved gen-delims, excluding '?'
1453             // gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
1454                 case ':':
1455                 case '/':
1456                 case '#':
1457                 case '[':
1458                 case ']':
1459                 case '@':
1460 
1461                     // reserved sub-delims, excluding ','
1462                     // sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
1463                     //               / "*" / "+" / "," / ";" / "="
1464                 case '!':
1465                 case '$':
1466                 case '&':
1467                 case '\'':
1468                 case '(':
1469                 case ')':
1470                 case '*':
1471                 case '+':
1472                 case ';':
1473                 case '=':
1474 
1475                     // unreserved
1476                     // unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
1477                 case 'a':
1478                 case 'b':
1479                 case 'c':
1480                 case 'd':
1481                 case 'e':
1482                 case 'f':
1483                 case 'g':
1484                 case 'h':
1485                 case 'i':
1486                 case 'j':
1487                 case 'k':
1488                 case 'l':
1489                 case 'm':
1490                 case 'n':
1491                 case 'o':
1492                 case 'p':
1493                 case 'q':
1494                 case 'r':
1495                 case 's':
1496                 case 't':
1497                 case 'u':
1498                 case 'v':
1499                 case 'w':
1500                 case 'x':
1501                 case 'y':
1502                 case 'z':
1503 
1504                 case 'A':
1505                 case 'B':
1506                 case 'C':
1507                 case 'D':
1508                 case 'E':
1509                 case 'F':
1510                 case 'G':
1511                 case 'H':
1512                 case 'I':
1513                 case 'J':
1514                 case 'K':
1515                 case 'L':
1516                 case 'M':
1517                 case 'N':
1518                 case 'O':
1519                 case 'P':
1520                 case 'Q':
1521                 case 'R':
1522                 case 'S':
1523                 case 'T':
1524                 case 'U':
1525                 case 'V':
1526                 case 'W':
1527                 case 'X':
1528                 case 'Y':
1529                 case 'Z':
1530 
1531                 case '0':
1532                 case '1':
1533                 case '2':
1534                 case '3':
1535                 case '4':
1536                 case '5':
1537                 case '6':
1538                 case '7':
1539                 case '8':
1540                 case '9':
1541 
1542                 case '-':
1543                 case '.':
1544                 case '_':
1545                 case '~':
1546 
1547                     sb.append( c );
1548                     break;
1549 
1550                 case ',':
1551 
1552                     // special case for comma
1553                     if ( doubleEncode )
1554                     {
1555                         sb.append( "%2c" );
1556                     }
1557                     else
1558                     {
1559                         sb.append( c );
1560                     }
1561                     break;
1562 
1563                 default:
1564 
1565                     // percent encoding
1566                     byte[] bytes = Unicode.charToBytes( c );
1567                     char[] hex = Strings.toHexString( bytes ).toCharArray();
1568                     for ( int j = 0; j < hex.length; j++ )
1569                     {
1570                         if ( j % 2 == 0 )
1571                         {
1572                             sb.append( '%' );
1573                         }
1574                         sb.append( hex[j] );
1575 
1576                     }
1577 
1578                     break;
1579             }
1580         }
1581 
1582         return sb.toString();
1583     }
1584 
1585 
1586     /**
1587      * Get a string representation of a LdapUrl.
1588      *
1589      * @return A LdapUrl string
1590      * @see LdapUrl#forceScopeRendering
1591      */
1592     @Override
1593     public String toString()
1594     {
1595         StringBuffer sb = new StringBuffer();
1596 
1597         sb.append( scheme );
1598 
1599         if ( host != null )
1600         {
1601             switch ( hostType )
1602             {
1603                 case IPV4 :
1604                 case REGULAR_NAME :
1605                     sb.append( host );
1606                     break;
1607                     
1608                 case IPV6 :
1609                 case IPV_FUTURE :
1610                     sb.append( '[' ).append( host ).append( ']' );
1611             }
1612         }
1613 
1614         if ( port != -1 )
1615         {
1616             sb.append( ':' ).append( port );
1617         }
1618 
1619         if ( dn != null )
1620         {
1621             sb.append( '/' ).append( urlEncode( dn.getName(), false ) );
1622 
1623             if ( ( attributes.size() != 0 ) || forceScopeRendering
1624                 || ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || ( extensionList.size() != 0 ) ) )
1625             {
1626                 sb.append( '?' );
1627 
1628                 boolean isFirst = true;
1629 
1630                 for ( String attribute : attributes )
1631                 {
1632                     if ( isFirst )
1633                     {
1634                         isFirst = false;
1635                     }
1636                     else
1637                     {
1638                         sb.append( ',' );
1639                     }
1640 
1641                     sb.append( urlEncode( attribute, false ) );
1642                 }
1643             }
1644 
1645             if ( forceScopeRendering )
1646             {
1647                 sb.append( '?' );
1648 
1649                 sb.append( scope.getLdapUrlValue() );
1650             }
1651             else
1652             {
1653                 if ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || ( extensionList.size() != 0 ) )
1654                 {
1655                     sb.append( '?' );
1656 
1657                     switch ( scope )
1658                     {
1659                         case ONELEVEL:
1660                         case SUBTREE:
1661                             sb.append( scope.getLdapUrlValue() );
1662                             break;
1663 
1664                         default:
1665                             break;
1666                     }
1667 
1668                     if ( ( filter != null ) || ( ( extensionList.size() != 0 ) ) )
1669                     {
1670                         sb.append( "?" );
1671 
1672                         if ( filter != null )
1673                         {
1674                             sb.append( urlEncode( filter, false ) );
1675                         }
1676 
1677                         if ( ( extensionList.size() != 0 ) )
1678                         {
1679                             sb.append( '?' );
1680 
1681                             boolean isFirst = true;
1682 
1683                             if ( extensionList.size() != 0 )
1684                             {
1685                                 for ( Extension extension : extensionList )
1686                                 {
1687                                     if ( !isFirst )
1688                                     {
1689                                         sb.append( ',' );
1690                                     }
1691                                     else
1692                                     {
1693                                         isFirst = false;
1694                                     }
1695 
1696                                     if ( extension.isCritical )
1697                                     {
1698                                         sb.append( '!' );
1699                                     }
1700                                     sb.append( urlEncode( extension.type, false ) );
1701 
1702                                     if ( extension.value != null )
1703                                     {
1704                                         sb.append( '=' );
1705                                         sb.append( urlEncode( extension.value, true ) );
1706                                     }
1707                                 }
1708                             }
1709                         }
1710                     }
1711                 }
1712             }
1713         }
1714         else
1715         {
1716             sb.append( '/' );
1717         }
1718 
1719         return sb.toString();
1720     }
1721 
1722 
1723     /**
1724      * @return Returns the attributes.
1725      */
1726     public List<String> getAttributes()
1727     {
1728         return attributes;
1729     }
1730 
1731 
1732     /**
1733      * @return Returns the dn.
1734      */
1735     public Dn getDn()
1736     {
1737         return dn;
1738     }
1739 
1740 
1741     /**
1742      * @return Returns the extensions.
1743      */
1744     public List<Extension> getExtensions()
1745     {
1746         return extensionList;
1747     }
1748 
1749 
1750     /**
1751      * Gets the extension.
1752      *
1753      * @param type the extension type, case-insensitive
1754      *
1755      * @return Returns the extension, null if this URL does not contain
1756      *         such an extension.
1757      */
1758     public Extension getExtension( String type )
1759     {
1760         for ( Extension extension : getExtensions() )
1761         {
1762             if ( extension.getType().equalsIgnoreCase( type ) )
1763             {
1764                 return extension;
1765             }
1766         }
1767         return null;
1768     }
1769 
1770 
1771     /**
1772      * Gets the extension value.
1773      *
1774      * @param type the extension type, case-insensitive
1775      *
1776      * @return Returns the extension value, null if this URL does not
1777      *         contain such an extension or if the extension value is null.
1778      */
1779     public String getExtensionValue( String type )
1780     {
1781         for ( Extension extension : getExtensions() )
1782         {
1783             if ( extension.getType().equalsIgnoreCase( type ) )
1784             {
1785                 return extension.getValue();
1786             }
1787         }
1788         return null;
1789     }
1790 
1791 
1792     /**
1793      * @return Returns the filter.
1794      */
1795     public String getFilter()
1796     {
1797         return filter;
1798     }
1799 
1800 
1801     /**
1802      * @return Returns the host.
1803      */
1804     public String getHost()
1805     {
1806         return host;
1807     }
1808 
1809 
1810     /**
1811      * @return Returns the port.
1812      */
1813     public int getPort()
1814     {
1815         return port;
1816     }
1817 
1818 
1819     /**
1820      * Returns the scope, one of {@link SearchScope#OBJECT},
1821      * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE}.
1822      *
1823      * @return Returns the scope.
1824      */
1825     public SearchScope getScope()
1826     {
1827         return scope;
1828     }
1829 
1830 
1831     /**
1832      * @return Returns the scheme.
1833      */
1834     public String getScheme()
1835     {
1836         return scheme;
1837     }
1838 
1839 
1840     /**
1841      * @return the number of bytes for this LdapUrl
1842      */
1843     public int getNbBytes()
1844     {
1845         return ( bytes != null ? bytes.length : 0 );
1846     }
1847 
1848 
1849     /**
1850      * @return a reference on the interned bytes representing this LdapUrl
1851      */
1852     public byte[] getBytesReference()
1853     {
1854         return bytes;
1855     }
1856 
1857 
1858     /**
1859      * @return a copy of the bytes representing this LdapUrl
1860      */
1861     public byte[] getBytesCopy()
1862     {
1863         if ( bytes != null )
1864         {
1865             byte[] copy = new byte[bytes.length];
1866             System.arraycopy( bytes, 0, copy, 0, bytes.length );
1867             return copy;
1868         }
1869         else
1870         {
1871             return null;
1872         }
1873     }
1874 
1875 
1876     /**
1877      * @return the LdapUrl as a String
1878      */
1879     public String getString()
1880     {
1881         return string;
1882     }
1883 
1884 
1885     /**
1886      * {@inheritDoc}
1887      */
1888     @Override
1889     public int hashCode()
1890     {
1891         return this.toString().hashCode();
1892     }
1893 
1894 
1895     /**
1896      * {@inheritDoc}
1897      */
1898     @Override
1899     public boolean equals( Object obj )
1900     {
1901         if ( this == obj )
1902         {
1903             return true;
1904         }
1905         if ( obj == null )
1906         {
1907             return false;
1908         }
1909         if ( getClass() != obj.getClass() )
1910         {
1911             return false;
1912         }
1913 
1914         final LdapUrl other = ( LdapUrl ) obj;
1915         return this.toString().equals( other.toString() );
1916     }
1917 
1918 
1919     /**
1920      * Sets the scheme. Must be "ldap://" or "ldaps://", otherwise "ldap://" is assumed as default.
1921      *
1922      * @param scheme the new scheme
1923      */
1924     public void setScheme( String scheme )
1925     {
1926         if ( ( ( scheme != null ) && LDAP_SCHEME.equals( scheme ) ) || LDAPS_SCHEME.equals( scheme ) )
1927         {
1928             this.scheme = scheme;
1929         }
1930         else
1931         {
1932             this.scheme = LDAP_SCHEME;
1933         }
1934 
1935     }
1936 
1937 
1938     /**
1939      * Sets the host.
1940      *
1941      * @param host the new host
1942      */
1943     public void setHost( String host )
1944     {
1945         this.host = host;
1946     }
1947 
1948 
1949     /**
1950      * Sets the port. Must be between 1 and 65535, otherwise -1 is assumed as default.
1951      *
1952      * @param port the new port
1953      */
1954     public void setPort( int port )
1955     {
1956         if ( ( port < 1 ) || ( port > 65535 ) )
1957         {
1958             this.port = -1;
1959         }
1960         else
1961         {
1962             this.port = port;
1963         }
1964     }
1965 
1966 
1967     /**
1968      * Sets the dn.
1969      *
1970      * @param dn the new dn
1971      */
1972     public void setDn( Dn dn )
1973     {
1974         this.dn = dn;
1975     }
1976 
1977 
1978     /**
1979      * Sets the attributes, null removes all existing attributes.
1980      *
1981      * @param attributes the new attributes
1982      */
1983     public void setAttributes( List<String> attributes )
1984     {
1985         if ( attributes == null )
1986         {
1987             this.attributes.clear();
1988         }
1989         else
1990         {
1991             this.attributes = attributes;
1992         }
1993     }
1994 
1995 
1996     /**
1997      * Sets the scope. Must be one of {@link SearchScope#OBJECT},
1998      * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE},
1999      * otherwise {@link SearchScope#OBJECT} is assumed as default.
2000      *
2001      * @param scope the new scope
2002      */
2003     public void setScope( int scope )
2004     {
2005         try
2006         {
2007             this.scope = SearchScope.getSearchScope( scope );
2008         }
2009         catch ( IllegalArgumentException iae )
2010         {
2011             this.scope = SearchScope.OBJECT;
2012         }
2013     }
2014 
2015 
2016     /**
2017      * Sets the scope. Must be one of {@link SearchScope#OBJECT},
2018      * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE},
2019      * otherwise {@link SearchScope#OBJECT} is assumed as default.
2020      *
2021      * @param scope the new scope
2022      */
2023     public void setScope( SearchScope scope )
2024     {
2025         if ( scope == null )
2026         {
2027             this.scope = SearchScope.OBJECT;
2028         }
2029         else
2030         {
2031             this.scope = scope;
2032         }
2033     }
2034 
2035 
2036     /**
2037      * Sets the filter.
2038      *
2039      * @param filter the new filter
2040      */
2041     public void setFilter( String filter )
2042     {
2043         this.filter = filter;
2044     }
2045 
2046 
2047     /**
2048      * If set to true forces the toString method to render the scope
2049      * regardless of optional nature.  Use this when you want explicit
2050      * search URL scope rendering.
2051      *
2052      * @param forceScopeRendering the forceScopeRendering to set
2053      */
2054     public void setForceScopeRendering( boolean forceScopeRendering )
2055     {
2056         this.forceScopeRendering = forceScopeRendering;
2057     }
2058 
2059     /**
2060      * An inner bean to hold extension information.
2061      *
2062      * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
2063          */
2064     public static class Extension
2065     {
2066         private boolean isCritical;
2067         private String type;
2068         private String value;
2069 
2070 
2071         /**
2072          * Creates a new instance of Extension.
2073          *
2074          * @param isCritical true for critical extension
2075          * @param type the extension type
2076          * @param value the extension value
2077          */
2078         public Extension( boolean isCritical, String type, String value )
2079         {
2080             super();
2081             this.isCritical = isCritical;
2082             this.type = type;
2083             this.value = value;
2084         }
2085 
2086 
2087         /**
2088          * Checks if is critical.
2089          *
2090          * @return true, if is critical
2091          */
2092         public boolean isCritical()
2093         {
2094             return isCritical;
2095         }
2096 
2097 
2098         /**
2099          * Sets the critical flag.
2100          *
2101          * @param critical the new critical flag
2102          */
2103         public void setCritical( boolean critical )
2104         {
2105             this.isCritical = critical;
2106         }
2107 
2108 
2109         /**
2110          * Gets the type.
2111          *
2112          * @return the type
2113          */
2114         public String getType()
2115         {
2116             return type;
2117         }
2118 
2119 
2120         /**
2121          * Sets the type.
2122          *
2123          * @param type the new type
2124          */
2125         public void setType( String type )
2126         {
2127             this.type = type;
2128         }
2129 
2130 
2131         /**
2132          * Gets the value.
2133          *
2134          * @return the value
2135          */
2136         public String getValue()
2137         {
2138             return value;
2139         }
2140 
2141 
2142         /**
2143          * Sets the value.
2144          *
2145          * @param value the new value
2146          */
2147         public void setValue( String value )
2148         {
2149             this.value = value;
2150         }
2151     }
2152 }