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