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  = "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "="
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     *
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     *   <reserved>
1532     *   <unreserved>
1533     *   <pct-encoded>
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 <dn>,
1544     *  <filter>, or other element of an LDAP URL.
1545     *
1546     *  It is a comma character ',' that occurs inside an <exvalue>.
1547     *
1548     *
1549     * RFC 3986, section 2.2 (Reserved Characters)
1550     *
1551     * reserved    = gen-delims / sub-delims
1552     * gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
1553     * sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
1554     *              / "*" / "+" / "," / ";" / "="
1555     *
1556     *
1557     * RFC 3986, section 2.3 (Unreserved Characters)
1558     *
1559     * unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
1560     *
1561     *
1562     * @param url The String to encode
1563     * @param doubleEncode Set if we need to encode the comma
1564     * @return An encoded string
1565     */
1566    public static String urlEncode( String url, boolean doubleEncode )
1567    {
1568        StringBuffer sb = new StringBuffer();
1569
1570        for ( int i = 0; i < url.length(); i++ )
1571        {
1572            char c = url.charAt( i );
1573
1574            switch ( c )
1575
1576            {
1577            // reserved and unreserved characters:
1578            // just append to the buffer
1579
1580            // reserved gen-delims, excluding '?'
1581            // gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
1582                case ':':
1583                case '/':
1584                case '#':
1585                case '[':
1586                case ']':
1587                case '@':
1588
1589                    // reserved sub-delims, excluding ','
1590                    // sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
1591                    //               / "*" / "+" / "," / ";" / "="
1592                case '!':
1593                case '$':
1594                case '&':
1595                case '\'':
1596                case '(':
1597                case ')':
1598                case '*':
1599                case '+':
1600                case ';':
1601                case '=':
1602
1603                    // unreserved
1604                    // unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
1605                case 'a':
1606                case 'b':
1607                case 'c':
1608                case 'd':
1609                case 'e':
1610                case 'f':
1611                case 'g':
1612                case 'h':
1613                case 'i':
1614                case 'j':
1615                case 'k':
1616                case 'l':
1617                case 'm':
1618                case 'n':
1619                case 'o':
1620                case 'p':
1621                case 'q':
1622                case 'r':
1623                case 's':
1624                case 't':
1625                case 'u':
1626                case 'v':
1627                case 'w':
1628                case 'x':
1629                case 'y':
1630                case 'z':
1631
1632                case 'A':
1633                case 'B':
1634                case 'C':
1635                case 'D':
1636                case 'E':
1637                case 'F':
1638                case 'G':
1639                case 'H':
1640                case 'I':
1641                case 'J':
1642                case 'K':
1643                case 'L':
1644                case 'M':
1645                case 'N':
1646                case 'O':
1647                case 'P':
1648                case 'Q':
1649                case 'R':
1650                case 'S':
1651                case 'T':
1652                case 'U':
1653                case 'V':
1654                case 'W':
1655                case 'X':
1656                case 'Y':
1657                case 'Z':
1658
1659                case '0':
1660                case '1':
1661                case '2':
1662                case '3':
1663                case '4':
1664                case '5':
1665                case '6':
1666                case '7':
1667                case '8':
1668                case '9':
1669
1670                case '-':
1671                case '.':
1672                case '_':
1673                case '~':
1674
1675                    sb.append( c );
1676                    break;
1677
1678                case ',':
1679
1680                    // special case for comma
1681                    if ( doubleEncode )
1682                    {
1683                        sb.append( "%2c" );
1684                    }
1685                    else
1686                    {
1687                        sb.append( c );
1688                    }
1689                    break;
1690
1691                default:
1692
1693                    // percent encoding
1694                    byte[] bytes = Unicode.charToBytes( c );
1695                    char[] hex = Strings.toHexString( bytes ).toCharArray();
1696                    for ( int j = 0; j < hex.length; j++ )
1697                    {
1698                        if ( j % 2 == 0 )
1699                        {
1700                            sb.append( '%' );
1701                        }
1702                        sb.append( hex[j] );
1703
1704                    }
1705
1706                    break;
1707            }
1708        }
1709
1710        return sb.toString();
1711    }
1712
1713
1714    /**
1715     * Get a string representation of a LdapUrl.
1716     *
1717     * @return A LdapUrl string
1718     * @see LdapUrl#forceScopeRendering
1719     */
1720    @Override
1721    public String toString()
1722    {
1723        StringBuffer sb = new StringBuffer();
1724
1725        sb.append( scheme );
1726
1727        if ( host != null )
1728        {
1729            switch ( hostType )
1730            {
1731                case IPV4:
1732                case REGULAR_NAME:
1733                    sb.append( host );
1734                    break;
1735
1736                case IPV6:
1737                case IPV_FUTURE:
1738                    sb.append( '[' ).append( host ).append( ']' );
1739                    break;
1740
1741                default:
1742                    throw new IllegalArgumentException( "Unexpected HostTypeEnum " + hostType );
1743            }
1744        }
1745
1746        if ( port != -1 )
1747        {
1748            sb.append( ':' ).append( port );
1749        }
1750
1751        if ( dn != null )
1752        {
1753            sb.append( '/' ).append( urlEncode( dn.getName(), false ) );
1754
1755            if ( ( attributes.size() != 0 ) || forceScopeRendering
1756                || ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || ( extensionList.size() != 0 ) ) )
1757            {
1758                sb.append( '?' );
1759
1760                boolean isFirst = true;
1761
1762                for ( String attribute : attributes )
1763                {
1764                    if ( isFirst )
1765                    {
1766                        isFirst = false;
1767                    }
1768                    else
1769                    {
1770                        sb.append( ',' );
1771                    }
1772
1773                    sb.append( urlEncode( attribute, false ) );
1774                }
1775            }
1776
1777            if ( forceScopeRendering )
1778            {
1779                sb.append( '?' );
1780
1781                sb.append( scope.getLdapUrlValue() );
1782            }
1783            else
1784            {
1785                if ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || ( extensionList.size() != 0 ) )
1786                {
1787                    sb.append( '?' );
1788
1789                    switch ( scope )
1790                    {
1791                        case ONELEVEL:
1792                        case SUBTREE:
1793                            sb.append( scope.getLdapUrlValue() );
1794                            break;
1795
1796                        default:
1797                            break;
1798                    }
1799
1800                    if ( ( filter != null ) || ( ( extensionList.size() != 0 ) ) )
1801                    {
1802                        sb.append( "?" );
1803
1804                        if ( filter != null )
1805                        {
1806                            sb.append( urlEncode( filter, false ) );
1807                        }
1808
1809                        if ( ( extensionList.size() != 0 ) )
1810                        {
1811                            sb.append( '?' );
1812
1813                            boolean isFirst = true;
1814
1815                            if ( extensionList.size() != 0 )
1816                            {
1817                                for ( Extension extension : extensionList )
1818                                {
1819                                    if ( !isFirst )
1820                                    {
1821                                        sb.append( ',' );
1822                                    }
1823                                    else
1824                                    {
1825                                        isFirst = false;
1826                                    }
1827
1828                                    if ( extension.isCritical )
1829                                    {
1830                                        sb.append( '!' );
1831                                    }
1832                                    sb.append( urlEncode( extension.type, false ) );
1833
1834                                    if ( extension.value != null )
1835                                    {
1836                                        sb.append( '=' );
1837                                        sb.append( urlEncode( extension.value, true ) );
1838                                    }
1839                                }
1840                            }
1841                        }
1842                    }
1843                }
1844            }
1845        }
1846        else
1847        {
1848            sb.append( '/' );
1849        }
1850
1851        return sb.toString();
1852    }
1853
1854
1855    /**
1856     * @return Returns the attributes.
1857     */
1858    public List<String> getAttributes()
1859    {
1860        return attributes;
1861    }
1862
1863
1864    /**
1865     * @return Returns the dn.
1866     */
1867    public Dn getDn()
1868    {
1869        return dn;
1870    }
1871
1872
1873    /**
1874     * @return Returns the extensions.
1875     */
1876    public List<Extension> getExtensions()
1877    {
1878        return extensionList;
1879    }
1880
1881
1882    /**
1883     * Gets the extension.
1884     *
1885     * @param type the extension type, case-insensitive
1886     *
1887     * @return Returns the extension, null if this URL does not contain
1888     *         such an extension.
1889     */
1890    public Extension getExtension( String type )
1891    {
1892        for ( Extension extension : getExtensions() )
1893        {
1894            if ( extension.getType().equalsIgnoreCase( type ) )
1895            {
1896                return extension;
1897            }
1898        }
1899        return null;
1900    }
1901
1902
1903    /**
1904     * Gets the extension value.
1905     *
1906     * @param type the extension type, case-insensitive
1907     *
1908     * @return Returns the extension value, null if this URL does not
1909     *         contain such an extension or if the extension value is null.
1910     */
1911    public String getExtensionValue( String type )
1912    {
1913        for ( Extension extension : getExtensions() )
1914        {
1915            if ( extension.getType().equalsIgnoreCase( type ) )
1916            {
1917                return extension.getValue();
1918            }
1919        }
1920        return null;
1921    }
1922
1923
1924    /**
1925     * @return Returns the filter.
1926     */
1927    public String getFilter()
1928    {
1929        return filter;
1930    }
1931
1932
1933    /**
1934     * @return Returns the host.
1935     */
1936    public String getHost()
1937    {
1938        return host;
1939    }
1940
1941
1942    /**
1943     * @return Returns the port.
1944     */
1945    public int getPort()
1946    {
1947        return port;
1948    }
1949
1950
1951    /**
1952     * Returns the scope, one of {@link SearchScope#OBJECT},
1953     * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE}.
1954     *
1955     * @return Returns the scope.
1956     */
1957    public SearchScope getScope()
1958    {
1959        return scope;
1960    }
1961
1962
1963    /**
1964     * @return Returns the scheme.
1965     */
1966    public String getScheme()
1967    {
1968        return scheme;
1969    }
1970
1971
1972    /**
1973     * @return the number of bytes for this LdapUrl
1974     */
1975    public int getNbBytes()
1976    {
1977        return ( bytes != null ? bytes.length : 0 );
1978    }
1979
1980
1981    /**
1982     * @return a reference on the interned bytes representing this LdapUrl
1983     */
1984    public byte[] getBytesReference()
1985    {
1986        return bytes;
1987    }
1988
1989
1990    /**
1991     * @return a copy of the bytes representing this LdapUrl
1992     */
1993    public byte[] getBytesCopy()
1994    {
1995        if ( bytes != null )
1996        {
1997            byte[] copy = new byte[bytes.length];
1998            System.arraycopy( bytes, 0, copy, 0, bytes.length );
1999            return copy;
2000        }
2001        else
2002        {
2003            return null;
2004        }
2005    }
2006
2007
2008    /**
2009     * @return the LdapUrl as a String
2010     */
2011    public String getString()
2012    {
2013        return string;
2014    }
2015
2016
2017    /**
2018     * {@inheritDoc}
2019     */
2020    @Override
2021    public int hashCode()
2022    {
2023        return this.toString().hashCode();
2024    }
2025
2026
2027    /**
2028     * {@inheritDoc}
2029     */
2030    @Override
2031    public boolean equals( Object obj )
2032    {
2033        if ( this == obj )
2034        {
2035            return true;
2036        }
2037        if ( obj == null )
2038        {
2039            return false;
2040        }
2041        if ( getClass() != obj.getClass() )
2042        {
2043            return false;
2044        }
2045
2046        final LdapUrl other = ( LdapUrl ) obj;
2047        return this.toString().equals( other.toString() );
2048    }
2049
2050
2051    /**
2052     * Sets the scheme. Must be "ldap://" or "ldaps://", otherwise "ldap://" is assumed as default.
2053     *
2054     * @param scheme the new scheme
2055     */
2056    public void setScheme( String scheme )
2057    {
2058        if ( ( ( scheme != null ) && LDAP_SCHEME.equals( scheme ) ) || LDAPS_SCHEME.equals( scheme ) )
2059        {
2060            this.scheme = scheme;
2061        }
2062        else
2063        {
2064            this.scheme = LDAP_SCHEME;
2065        }
2066
2067    }
2068
2069
2070    /**
2071     * Sets the host.
2072     *
2073     * @param host the new host
2074     */
2075    public void setHost( String host )
2076    {
2077        this.host = host;
2078    }
2079
2080
2081    /**
2082     * Sets the port. Must be between 1 and 65535, otherwise -1 is assumed as default.
2083     *
2084     * @param port the new port
2085     */
2086    public void setPort( int port )
2087    {
2088        if ( ( port < 1 ) || ( port > 65535 ) )
2089        {
2090            this.port = -1;
2091        }
2092        else
2093        {
2094            this.port = port;
2095        }
2096    }
2097
2098
2099    /**
2100     * Sets the dn.
2101     *
2102     * @param dn the new dn
2103     */
2104    public void setDn( Dn dn )
2105    {
2106        this.dn = dn;
2107    }
2108
2109
2110    /**
2111     * Sets the attributes, null removes all existing attributes.
2112     *
2113     * @param attributes the new attributes
2114     */
2115    public void setAttributes( List<String> attributes )
2116    {
2117        if ( attributes == null )
2118        {
2119            this.attributes.clear();
2120        }
2121        else
2122        {
2123            this.attributes = attributes;
2124        }
2125    }
2126
2127
2128    /**
2129     * Sets the scope. Must be one of {@link SearchScope#OBJECT},
2130     * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE},
2131     * otherwise {@link SearchScope#OBJECT} is assumed as default.
2132     *
2133     * @param scope the new scope
2134     */
2135    public void setScope( int scope )
2136    {
2137        try
2138        {
2139            this.scope = SearchScope.getSearchScope( scope );
2140        }
2141        catch ( IllegalArgumentException iae )
2142        {
2143            this.scope = SearchScope.OBJECT;
2144        }
2145    }
2146
2147
2148    /**
2149     * Sets the scope. Must be one of {@link SearchScope#OBJECT},
2150     * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE},
2151     * otherwise {@link SearchScope#OBJECT} is assumed as default.
2152     *
2153     * @param scope the new scope
2154     */
2155    public void setScope( SearchScope scope )
2156    {
2157        if ( scope == null )
2158        {
2159            this.scope = SearchScope.OBJECT;
2160        }
2161        else
2162        {
2163            this.scope = scope;
2164        }
2165    }
2166
2167
2168    /**
2169     * Sets the filter.
2170     *
2171     * @param filter the new filter
2172     */
2173    public void setFilter( String filter )
2174    {
2175        this.filter = filter;
2176    }
2177
2178
2179    /**
2180     * If set to true forces the toString method to render the scope
2181     * regardless of optional nature.  Use this when you want explicit
2182     * search URL scope rendering.
2183     *
2184     * @param forceScopeRendering the forceScopeRendering to set
2185     */
2186    public void setForceScopeRendering( boolean forceScopeRendering )
2187    {
2188        this.forceScopeRendering = forceScopeRendering;
2189    }
2190
2191    /**
2192     * An inner bean to hold extension information.
2193     *
2194     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
2195         */
2196    public static class Extension
2197    {
2198        private boolean isCritical;
2199        private String type;
2200        private String value;
2201
2202
2203        /**
2204         * Creates a new instance of Extension.
2205         *
2206         * @param isCritical true for critical extension
2207         * @param type the extension type
2208         * @param value the extension value
2209         */
2210        public Extension( boolean isCritical, String type, String value )
2211        {
2212            super();
2213            this.isCritical = isCritical;
2214            this.type = type;
2215            this.value = value;
2216        }
2217
2218
2219        /**
2220         * Checks if is critical.
2221         *
2222         * @return true, if is critical
2223         */
2224        public boolean isCritical()
2225        {
2226            return isCritical;
2227        }
2228
2229
2230        /**
2231         * Sets the critical flag.
2232         *
2233         * @param critical the new critical flag
2234         */
2235        public void setCritical( boolean critical )
2236        {
2237            this.isCritical = critical;
2238        }
2239
2240
2241        /**
2242         * Gets the type.
2243         *
2244         * @return the type
2245         */
2246        public String getType()
2247        {
2248            return type;
2249        }
2250
2251
2252        /**
2253         * Sets the type.
2254         *
2255         * @param type the new type
2256         */
2257        public void setType( String type )
2258        {
2259            this.type = type;
2260        }
2261
2262
2263        /**
2264         * Gets the value.
2265         *
2266         * @return the value
2267         */
2268        public String getValue()
2269        {
2270            return value;
2271        }
2272
2273
2274        /**
2275         * Sets the value.
2276         *
2277         * @param value the new value
2278         */
2279        public void setValue( String value )
2280        {
2281            this.value = value;
2282        }
2283    }
2284}