001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.directory.api.ldap.model.url;
021
022
023import java.io.ByteArrayOutputStream;
024import java.nio.charset.StandardCharsets;
025import java.text.ParseException;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Set;
031import java.util.regex.Matcher;
032import java.util.regex.Pattern;
033
034import org.apache.directory.api.i18n.I18n;
035import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
036import org.apache.directory.api.ldap.model.exception.LdapURLEncodingException;
037import org.apache.directory.api.ldap.model.exception.LdapUriException;
038import org.apache.directory.api.ldap.model.exception.UrlDecoderException;
039import org.apache.directory.api.ldap.model.filter.FilterParser;
040import org.apache.directory.api.ldap.model.message.SearchScope;
041import org.apache.directory.api.ldap.model.name.Dn;
042import org.apache.directory.api.util.Chars;
043import org.apache.directory.api.util.StringConstants;
044import org.apache.directory.api.util.Strings;
045import org.apache.directory.api.util.Unicode;
046
047
048/**
049 * Decodes a LdapUrl, and checks that it complies with
050 * the RFC 4516. The grammar is the following :
051 * <pre>
052 * ldapurl    = scheme "://" [host [ ":" port]] ["/"
053 *                   dn ["?" [attributes] ["?" [scope]
054 *                   ["?" [filter] ["?" extensions]]]]]
055 * scheme     = "ldap"
056 * dn         = Dn
057 * attributes = attrdesc ["," attrdesc]*
058 * attrdesc   = selector ["," selector]*
059 * selector   = attributeSelector (from Section 4.5.1 of RFC4511)
060 * scope      = "base" / "one" / "sub"
061 * extensions = extension ["," extension]*
062 * extension  = ["!"] extype ["=" exvalue]
063 * extype     = oid (from Section 1.4 of RFC4512)
064 * exvalue    = LDAPString (from Section 4.1.2 of RFC4511)
065 * host       = host from Section 3.2.2 of RFC3986
066 * port       = port from Section 3.2.3 of RFC3986
067 * filter     = filter from Section 3 of RFC 4515
068 * </pre>
069 * 
070 * From Section 3.2.1/2 of RFC3986
071 * <pre>
072 * host        = IP-literal / IPv4address / reg-name
073 * port        = *DIGIT
074 * IP-literal  = "[" ( IPv6address / IPvFuture  ) "]"
075 * IPvFuture   = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
076 * IPv6address = 6( h16 ":" ) ls32 
077 *               | "::" 5( h16 ":" ) ls32
078 *               | [               h16 ] "::" 4( h16 ":" ) ls32
079 *               | [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
080 *               | [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
081 *               | [ *3( h16 ":" ) h16 ] "::"    h16 ":"   ls32
082 *               | [ *4( h16 ":" ) h16 ] "::"              ls32
083 *               | [ *5( h16 ":" ) h16 ] "::"              h16
084 *               | [ *6( h16 ":" ) h16 ] "::"
085 * IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
086 * dec-octet   = DIGIT | [1-9] DIGIT | "1" 2DIGIT | "2" [0-4] DIGIT | "25" [0-5]
087 * reg-name    = *( unreserved / pct-encoded / sub-delims )
088 * unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
089 * pct-encoded = "%" HEXDIG HEXDIG
090 * sub-delims  = "!" | "$" | "&amp;" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "="
091 * h16         = 1*4HEXDIG
092 * ls32        = ( h16 ":" h16 ) / IPv4address
093 * DIGIT       = 0..9
094 * ALPHA       = A-Z / a-z
095 * HEXDIG      = DIGIT / A-F / a-f
096 * </pre>
097 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
098 */
099public 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}