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