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