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.text.ParseException;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Set;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032
033import org.apache.directory.api.i18n.I18n;
034import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
035import org.apache.directory.api.ldap.model.exception.LdapURLEncodingException;
036import org.apache.directory.api.ldap.model.exception.LdapUriException;
037import org.apache.directory.api.ldap.model.exception.UrlDecoderException;
038import org.apache.directory.api.ldap.model.filter.FilterParser;
039import org.apache.directory.api.ldap.model.message.SearchScope;
040import org.apache.directory.api.ldap.model.name.Dn;
041import org.apache.directory.api.util.Chars;
042import org.apache.directory.api.util.StringConstants;
043import org.apache.directory.api.util.Strings;
044import org.apache.directory.api.util.Unicode;
045
046
047/**
048 * Decodes a LdapUrl, and checks that it complies with
049 * the RFC 4516. The grammar is the following :
050 * <pre>
051 * ldapurl    = scheme "://" [host [ ":" port]] ["/"
052 *                   dn ["?" [attributes] ["?" [scope]
053 *                   ["?" [filter] ["?" extensions]]]]]
054 * scheme     = "ldap"
055 * dn         = Dn
056 * attributes = attrdesc ["," attrdesc]*
057 * attrdesc   = selector ["," selector]*
058 * selector   = attributeSelector (from Section 4.5.1 of RFC4511)
059 * scope      = "base" / "one" / "sub"
060 * extensions = extension ["," extension]*
061 * extension  = ["!"] extype ["=" exvalue]
062 * extype     = oid (from Section 1.4 of RFC4512)
063 * exvalue    = LDAPString (from Section 4.1.2 of RFC4511)
064 * host       = host from Section 3.2.2 of RFC3986
065 * port       = port from Section 3.2.3 of RFC3986
066 * filter     = filter from Section 3 of RFC 4515
067 * </pre>
068 * 
069 * From Section 3.2.1/2 of RFC3986
070 * <pre>
071 * host        = IP-literal / IPv4address / reg-name
072 * port        = *DIGIT
073 * IP-literal  = "[" ( IPv6address / IPvFuture  ) "]"
074 * IPvFuture   = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
075 * IPv6address = 6( h16 ":" ) ls32 
076 *               | "::" 5( h16 ":" ) ls32
077 *               | [               h16 ] "::" 4( h16 ":" ) ls32
078 *               | [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
079 *               | [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
080 *               | [ *3( h16 ":" ) h16 ] "::"    h16 ":"   ls32
081 *               | [ *4( h16 ":" ) h16 ] "::"              ls32
082 *               | [ *5( h16 ":" ) h16 ] "::"              h16
083 *               | [ *6( h16 ":" ) h16 ] "::"
084 * IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
085 * dec-octet   = DIGIT | [1-9] DIGIT | "1" 2DIGIT | "2" [0-4] DIGIT | "25" [0-5]
086 * reg-name    = *( unreserved / pct-encoded / sub-delims )
087 * unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
088 * pct-encoded = "%" HEXDIG HEXDIG
089 * sub-delims  = "!" | "$" | "&amp;" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "="
090 * h16         = 1*4HEXDIG
091 * ls32        = ( h16 ":" h16 ) / IPv4address
092 * DIGIT       = 0..9
093 * ALPHA       = A-Z / a-z
094 * HEXDIG      = DIGIT / A-F / a-f
095 * </pre>
096 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
097 */
098public class LdapUrl
099{
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}