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